---
name: drawer
title: Drawer
url: https://ui.frontic.com/components/ui/drawer.md
tier: ui
install: npx @frontic/ui add drawer
files:
  - components/ui/drawer/Drawer.vue
  - components/ui/drawer/DrawerBody.vue
  - components/ui/drawer/DrawerClose.vue
  - components/ui/drawer/DrawerContent.vue
  - components/ui/drawer/DrawerDescription.vue
  - components/ui/drawer/DrawerFooter.vue
  - components/ui/drawer/DrawerHeader.vue
  - components/ui/drawer/DrawerOverlay.vue
  - components/ui/drawer/DrawerTitle.vue
  - components/ui/drawer/DrawerTrigger.vue
  - components/ui/drawer/index.ts
dependencies:
  - "@vueuse/core"
  - class-variance-authority
  - reka-ui
---

# Drawer

A panel that slides in from any edge and closes with a swipe, with snap points for a half-open state, such as filters or a mini cart on a phone.

## Install

```sh
npx @frontic/ui add drawer
```

npm packages: `@vueuse/core`, `class-variance-authority`, `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { XIcon } from '@lucide/vue'

import { ref } from 'vue'
import { Button } from '@/components/ui/button'
import { Image } from '@/components/ui/image'
import { Price } from '@/components/ui/price'
import { Drawer, DrawerBody, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger } from '@/components/ui/drawer'

const image = (name: string) => `https://static.frontic.com/cdn-cgi/image/width=640,format=auto/ui/products/${name}.jpg`

const jackets = [
  {
    key: 'frost-escape-parka',
    name: 'Frost Escape Parka',
    href: 'https://sw.demo-shop.com/frost-escape-parka-3abd4b58',
    image: image('frost-escape-parka'),
    price: { amount: 14900, currency: 'EUR' },
    specs: { 'Made for': 'Ski', Material: 'Fleece', Colour: 'Sun yellow' },
  },
  {
    key: 'blizzard-defender',
    name: 'Blizzard Defender',
    href: 'https://sw.demo-shop.com/blizzard-defender-455fb5a3',
    image: image('blizzard-defender-aubergine'),
    price: { amount: 12600, currency: 'EUR' },
    specs: { 'Made for': 'Snowboard', Material: 'Nylon', Colour: 'Aubergine' },
  },
  {
    key: 'northern-light-hybrid-jacket',
    name: 'Northern Light Hybrid Jacket',
    href: 'https://sw.demo-shop.com/nordlicht-hybrid-jacket-558734f9',
    image: image('northern-light-hybrid-jacket-charcoal'),
    price: { amount: 11500, currency: 'EUR' },
    specs: { 'Made for': 'Outdoor', Material: 'Polyester', Colour: 'Charcoal' },
  },
]
const specs = ['Made for', 'Material', 'Colour'] as const

const compared = ref(jackets.map((jacket) => jacket.key))
const shown = () => jackets.filter((jacket) => compared.value.includes(jacket.key))
const remove = (key: string) => (compared.value = compared.value.filter((k) => k !== key))
</script>

<template>
  <Drawer default-open>
    <DrawerTrigger as-child>
      <Button variant="outline" color="neutral">Compare ({{ shown().length }})</Button>
    </DrawerTrigger>
    <DrawerContent>
      <DrawerHeader>
        <div class="mx-auto flex w-full max-w-2xl flex-col gap-1 @md:gap-2">
          <DrawerTitle>Compare jackets</DrawerTitle>
          <DrawerDescription>{{ shown().length }} of 4 places taken. Add more from any product page.</DrawerDescription>
        </div>
      </DrawerHeader>
      <DrawerBody>
        <p v-if="!shown().length" class="mx-auto max-w-2xl py-8 text-center text-muted-foreground">Nothing to compare yet.</p>
        <div v-else class="mx-auto grid max-w-2xl grid-cols-3 gap-x-4 gap-y-3">
          <div v-for="jacket in shown()" :key="jacket.key" class="relative flex flex-col gap-2">
            <Image :src="jacket.image" :alt="jacket.name" class="aspect-square w-full object-contain" />
            <Button variant="ghost" color="neutral" size="icon-sm" class="absolute top-1 inset-e-1" @click="remove(jacket.key)">
              <XIcon data-icon />
              <span class="sr-only">Remove {{ jacket.name }}</span>
            </Button>
            <a :href="jacket.href" class="font-medium">{{ jacket.name }}</a>
            <Price :price="jacket.price" />
          </div>
          <template v-for="spec in specs" :key="spec">
            <p class="col-span-3 mt-2 border-t border-border pt-3 text-xs font-semibold tracking-wide uppercase">{{ spec }}</p>
            <p v-for="jacket in shown()" :key="jacket.key">{{ jacket.specs[spec] }}</p>
          </template>
        </div>
      </DrawerBody>
      <DrawerFooter>
        <div class="mx-auto flex w-full max-w-2xl flex-col gap-2 @md:flex-row @md:justify-end">
          <Button variant="outline" color="neutral" :disabled="!shown().length" @click="compared = []">Clear all</Button>
          <DrawerClose as-child><Button>Done</Button></DrawerClose>
        </div>
      </DrawerFooter>
    </DrawerContent>
  </Drawer>
</template>
```

The example also uses `button`, `image` and `price`, which `add drawer` leaves out: `npx @frontic/ui add button image price`.

## Anatomy

```vue
<Drawer>
  <DrawerTrigger />
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle />
      <DrawerDescription />
    </DrawerHeader>
    <DrawerBody />
    <DrawerFooter>
      <DrawerClose />
    </DrawerFooter>
  </DrawerContent>
</Drawer>
```

## API reference

### Drawer

Groups all parts of the drawer.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` |  | v-model:open |
| `defaultOpen` | `boolean` | `false` | Opens the drawer on first render when it is uncontrolled. |
| `modal` | `DrawerModal`: `boolean \| 'trap-focus'` | `true` | Modality of the drawer.<br>- `true` (default): modal with focus trap, scroll lock, and outside-press dismiss<br>- `'trap-focus'`: traps focus but allows outside pointer events (non-modal side panels)<br>- `false`: non-modal |
| `swipeDirection` | `SwipeDirection`: `'right' \| 'left' \| 'up' \| 'down'` | `'down'` | Direction to swipe to dismiss. |
| `snapPoints` | `DrawerSnapPoint[]` |  | Preset snap positions (fractions 0-1, pixels >1, or '148px'/'30rem' strings) |
| `snapPoint` | `DrawerSnapPoint \| null`: `null \| string \| number` |  | v-model:snapPoint |
| `defaultSnapPoint` | `DrawerSnapPoint \| null`: `null \| string \| number` |  | The snap point the drawer opens at when uncontrolled: a fraction of the screen, or a pixel value. |
| `snapToSequentialPoints` | `boolean` | `false` | When true, snaps to the next sequential snap point (one step at a time). When false, snaps to the nearest snap point by distance. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:open` | `value: boolean, details: DrawerOpenChangeDetails` | Fires with the new value when `open` changes; `v-model:open` binds it. |
| `update:openComplete` | `value: boolean` | Fires with the new value when `openComplete` changes; `v-model:openComplete` binds it. |
| `update:snapPoint` | `value: DrawerSnapPoint \| null` | Fires with the new value when `snapPoint` changes; `v-model:snapPoint` binds it. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ open: boolean; close: () => void; }` | Holds the drawer's trigger and content; gets whether it is open and a function that closes it. |

### DrawerBody

Holds the drawer's main content and scrolls it when the drawer reaches its height cap, keeping the header and footer in view. Renders a `<div>`.

`data-slot="drawer-body"` · `DrawerBody.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### DrawerClose

Closes the drawer. Renders a `<button>`.

`data-slot="drawer-close"` · `DrawerClose.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'button'` | The element or component this component should render as. Can be overwritten by `asChild`. |

### DrawerContent

Holds what the drawer shows while open.

`data-slot="drawer-content"` · `DrawerContent.vue`

Styling: `drawer-content`, which extends [`overlay-sheet`](https://ui.frontic.com/DESIGN.md#overlay-sheet). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `forceMount` | `boolean` | `false` | Keeps the content mounted while closed, for controlling its animation yourself. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` |  | The element or component this component should render as. Can be overwritten by `asChild`. |
| `disableOutsidePointerEvents` | `boolean` | `false` | When `true`, hover/focus/click interactions will be disabled on elements outside the `DismissableLayer`. Users will need to click twice on outside elements to interact with them: once to close the `DismissableLayer`, and again to trigger the element. |
| `initialFocus` | `boolean \| HTMLElement \| null`: `null \| boolean \| HTMLElement` | `true` | Initial focus target when the drawer opens.<br>- `true` / default: focus the first focusable element inside<br>- `false`: do not focus anything<br>- element ref: focus that specific element |
| `finalFocus` | `boolean \| HTMLElement \| null`: `null \| boolean \| HTMLElement` | `true` | Final focus target when the drawer closes.<br>- `true` / default: focus the trigger<br>- `false`: do not restore focus<br>- element ref: focus that specific element |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Event | Payload | Description |
| --- | --- | --- |
| `escapeKeyDown` | `event: KeyboardEvent` | Event handler called when the escape key is down. Can be prevented. |
| `pointerDownOutside` | `event: PointerDownOutsideEvent` | Event handler called when a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
| `focusOutside` | `event: FocusOutsideEvent` | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
| `interactOutside` | `event: PointerDownOutsideEvent \| FocusOutsideEvent` | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown` event happens outside or focus moves outside of it. Can be prevented. |
| `openAutoFocus` | `event: Event` | Fires when focus moves into the content as it opens; call `preventDefault()` to keep focus where it is. |
| `closeAutoFocus` | `event: Event` | Fires when focus moves back to the trigger as the content closes; call `preventDefault()` to keep focus where it is. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-dismissable-layer` |  | Marks a layer that closes on Escape or a click outside it. |
| `data-nested-drawer-open` |  | Present while another drawer is open on top of this one. |
| `data-state` | `'open' \| 'closed'` | The state the part is in, one value per state. |
| `data-swipe-direction` | `'up' \| 'down' \| 'left' \| 'right'` | The direction the drawer is swiped to close, which is also the edge it slides in from. |
| `data-swiping` |  | Present while the drawer is being dragged. |

| CSS variable | Description |
| --- | --- |
| `--drawer-frontmost-height` | The height of the frontmost drawer in a stack, so the ones behind can match it. |
| `--drawer-height` | The drawer's height, in pixels. |
| `--drawer-snap-point-offset` | How far the drawer sits from fully open at its current snap point, in pixels. |
| `--drawer-swipe-movement-x` | How far the drawer has been dragged sideways, in pixels. |
| `--drawer-swipe-movement-y` | How far the drawer has been dragged up or down, in pixels. |
| `--drawer-swipe-progress` | How far a swipe has gone towards closing the drawer, from 0 to 1. |
| `--drawer-swipe-strength` | How fast the last swipe was released, from 0.1 to 1, to scale the closing transition. |
| `--nested-drawers` | How many drawers are open on top of this one, for scaling it back behind them. |

### DrawerDescription

Explains the purpose of the drawer below its title. Renders a `<p>`.

`data-slot="drawer-description"` · `DrawerDescription.vue`

Styling: `drawer-description`, which extends [`part-description`](https://ui.frontic.com/DESIGN.md#part-description). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'p'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### DrawerFooter

Holds the actions of the drawer, such as apply and cancel buttons. Renders a `<div>`.

`data-slot="drawer-footer"` · `DrawerFooter.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### DrawerHeader

Groups the title and description at the top of the drawer. Renders a `<div>`.

`data-slot="drawer-header"` · `DrawerHeader.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### DrawerOverlay

Covers the page behind the open drawer.

`data-slot="drawer-overlay"` · `DrawerOverlay.vue`

Styling: `drawer-overlay`, which extends [`overlay-backdrop`](https://ui.frontic.com/DESIGN.md#overlay-backdrop). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `forceMount` | `boolean` | `false` | Keep mounted for animation control. |
| `forceRender` | `boolean` | `false` | Render even when inside a nested drawer. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` |  | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-state` | `'open' \| 'closed'` | The state the part is in, one value per state. |
| `data-swipe-direction` | `'up' \| 'down' \| 'left' \| 'right'` | The direction the drawer is swiped to close, which is also the edge it slides in from. |
| `data-swiping` |  | Present while the drawer is being dragged. |

### DrawerTitle

Names the drawer. Renders a `<h2>`.

`data-slot="drawer-title"` · `DrawerTitle.vue`

Styling: `drawer-title`, which extends [`part-title`](https://ui.frontic.com/DESIGN.md#part-title). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'h2'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### DrawerTrigger

Opens the drawer. Renders a `<button>`.

`data-slot="drawer-trigger"` · `DrawerTrigger.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'button'` | The element or component this component should render as. Can be overwritten by `asChild`. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-state` | `'open' \| 'closed'` | The state the part is in, one value per state. |

## Styling

| Part | Recipe |
| --- | --- |
| `header` | [`drawer-header`](https://ui.frontic.com/DESIGN.md#drawer-header) |
| `footer` | [`drawer-footer`](https://ui.frontic.com/DESIGN.md#drawer-footer) |
| `handle` | [`drawer-handle`](https://ui.frontic.com/DESIGN.md#drawer-handle) |
| `overlay` | [`drawer-overlay`](https://ui.frontic.com/DESIGN.md#drawer-overlay) |
| `title` | [`drawer-title`](https://ui.frontic.com/DESIGN.md#drawer-title) |
| `description` | [`drawer-description`](https://ui.frontic.com/DESIGN.md#drawer-description) |
| `content` | [`drawer-content`](https://ui.frontic.com/DESIGN.md#drawer-content) |
| `body` | [`drawer-body`](https://ui.frontic.com/DESIGN.md#drawer-body) |

## Source

`components/ui/drawer/Drawer.vue`

```vue
<script lang="ts">
/** Groups all parts of the drawer. */
export default {}
</script>

<script lang="ts" setup>
import type { DrawerRootEmits, DrawerRootProps } from 'reka-ui'
import { DrawerRoot, useForwardPropsEmits } from 'reka-ui'

const props = defineProps<DrawerRootProps>()

const emits = defineEmits<DrawerRootEmits>()

defineSlots<{
  /** Holds the drawer's trigger and content; gets whether it is open and a function that closes it. */
  default?: (props: { open: boolean; close: () => void }) => unknown
}>()

const forwarded = useForwardPropsEmits(props, emits)
</script>

<template>
  <DrawerRoot v-slot="slotProps" v-bind="forwarded">
    <slot v-bind="slotProps" />
  </DrawerRoot>
</template>
```

`components/ui/drawer/DrawerBody.vue`

```vue
<script lang="ts">
/** Holds the drawer's main content and scrolls it when the drawer reaches its height cap, keeping the header and footer in view. */
export default {}
</script>

<script setup lang="ts">
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
import type { HTMLAttributes } from 'vue'

const props = withDefaults(
  defineProps<
    PrimitiveProps & {
      class?: HTMLAttributes['class']
    }
  >(),
  { as: 'div' },
)
</script>

<template>
  <Primitive :as="props.as" :as-child="props.asChild" data-slot="drawer-body" :class="['fui-drawer-body overflow-y-auto', props.class]">
    <slot />
  </Primitive>
</template>
```

`components/ui/drawer/DrawerClose.vue`

```vue
<script lang="ts">
/** Closes the drawer. */
export default {}
</script>

<script lang="ts" setup>
import type { DrawerCloseProps } from 'reka-ui'
import { DrawerClose, useForwardProps } from 'reka-ui'

const props = defineProps<DrawerCloseProps>()
const forwarded = useForwardProps(props)
</script>

<template>
  <DrawerClose data-slot="drawer-close" v-bind="forwarded">
    <slot />
  </DrawerClose>
</template>
```

`components/ui/drawer/DrawerContent.vue`

```vue
<script lang="ts">
/** Holds what the drawer shows while open. */
export default {}
</script>

<script lang="ts" setup>
import type { DrawerContentEmits, DrawerContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { cva } from 'class-variance-authority'
import { DrawerContent, DrawerHandle, DrawerPortal, injectDrawerRootContext, useForwardPropsEmits } from 'reka-ui'
import DrawerOverlay from './DrawerOverlay.vue'

defineOptions({
  inheritAttrs: false,
})

const props = defineProps<DrawerContentProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<DrawerContentEmits>()

const delegatedProps = reactiveOmit(props, 'class')

const forwarded = useForwardPropsEmits(delegatedProps, emits)
const root = injectDrawerRootContext()
const content = cva(
  'fui-drawer-content group/drawer-content @container fixed z-50 will-change-transform data-open:animate-in data-closed:animate-out transition-transform duration-500 ease-out data-swiping:duration-0 data-swiping:select-none',
  {
    variants: {
      direction: {
        down: 'fui-drawer-content-down',
        up: 'fui-drawer-content-up',
        left: 'fui-drawer-content-left',
        right: 'fui-drawer-content-right',
      },
    },
    defaultVariants: { direction: 'down' },
  },
)
const DIRECTION_LAYOUT = {
  down: 'data-open:slide-in-from-bottom-full data-closed:slide-out-to-bottom-full inset-x-0 bottom-0 mt-24 max-h-4/5',
  up: 'data-open:slide-in-from-top-full data-closed:slide-out-to-top-full inset-x-0 top-0 mb-24 max-h-4/5',
  left: 'data-open:slide-in-from-left-full data-closed:slide-out-to-left-full inset-y-0 left-0 max-w-3/4',
  right: 'data-open:slide-in-from-right-full data-closed:slide-out-to-right-full inset-y-0 right-0 max-w-3/4',
}
</script>

<template>
  <DrawerPortal>
    <DrawerOverlay />
    <DrawerContent
      data-slot="drawer-content"
      v-bind="{ ...$attrs, ...forwarded }"
      :style="{ transform: 'translate3d(var(--drawer-swipe-movement-x, 0px), var(--drawer-swipe-movement-y, 0px), 0)' }"
      :class="[content({ direction: root.swipeDirection.value }), DIRECTION_LAYOUT[root.swipeDirection.value], props.class]"
    >
      <DrawerHandle v-if="root.swipeDirection.value === 'down'" class="fui-drawer-handle mx-auto shrink-0" />
      <slot />
    </DrawerContent>
  </DrawerPortal>
</template>
```

`components/ui/drawer/DrawerDescription.vue`

```vue
<script lang="ts">
/** Explains the purpose of the drawer below its title. */
export default {}
</script>

<script lang="ts" setup>
import type { DrawerDescriptionProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DrawerDescription, useForwardProps } from 'reka-ui'

const props = defineProps<DrawerDescriptionProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
</script>

<template>
  <DrawerDescription data-slot="drawer-description" v-bind="forwarded" :class="['fui-drawer-description', props.class]">
    <slot />
  </DrawerDescription>
</template>
```

`components/ui/drawer/DrawerFooter.vue`

```vue
<script lang="ts">
/** Holds the actions of the drawer, such as apply and cancel buttons. */
export default {}
</script>

<script lang="ts" setup>
import type { PrimitiveProps } from 'reka-ui'
import { Primitive } from 'reka-ui'
import type { HTMLAttributes } from 'vue'

const props = withDefaults(
  defineProps<
    PrimitiveProps & {
      class?: HTMLAttributes['class']
    }
  >(),
  { as: 'div' },
)
</script>

<template>
  <Primitive :as="props.as" :as-child="props.asChild" data-slot="drawer-footer" :class="['fui-drawer-footer', props.class]">
    <slot />
  </Primitive>
</template>
```

`components/ui/drawer/DrawerHeader.vue`

```vue
<script lang="ts">
/** Groups the title and description at the top of the drawer. */
export default {}
</script>

<script lang="ts" setup>
import type { PrimitiveProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { cva } from 'class-variance-authority'
import { injectDrawerRootContext, Primitive } from 'reka-ui'

const props = withDefaults(
  defineProps<
    PrimitiveProps & {
      class?: HTMLAttributes['class']
    }
  >(),
  { as: 'div' },
)

const root = injectDrawerRootContext()

const drawerHeader = cva('fui-drawer-header', {
  variants: { swipe: { vertical: 'fui-drawer-header-vertical', horizontal: 'fui-drawer-header-horizontal' } },
  defaultVariants: { swipe: 'vertical' },
})
</script>

<template>
  <Primitive
    :as="props.as"
    :as-child="props.asChild"
    data-slot="drawer-header"
    :class="[drawerHeader({ swipe: root.swipeDirection.value === 'down' || root.swipeDirection.value === 'up' ? 'vertical' : 'horizontal' }), props.class]"
  >
    <slot />
  </Primitive>
</template>
```

`components/ui/drawer/DrawerOverlay.vue`

```vue
<script lang="ts">
/** Covers the page behind the open drawer. */
export default {}
</script>

<script lang="ts" setup>
import type { DrawerOverlayProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DrawerOverlay, useForwardProps } from 'reka-ui'

const props = defineProps<DrawerOverlayProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
</script>

<template>
  <DrawerOverlay
    data-slot="drawer-overlay"
    v-bind="forwarded"
    :class="['fui-drawer-overlay data-open:animate-in data-closed:animate-out data-open:fade-in-0 data-closed:fade-out-0 fixed inset-0 z-50 supports-backdrop-filter:backdrop-blur-sm', props.class]"
  />
</template>
```

`components/ui/drawer/DrawerTitle.vue`

```vue
<script lang="ts">
/** Names the drawer. */
export default {}
</script>

<script lang="ts" setup>
import type { DrawerTitleProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { DrawerTitle, useForwardProps } from 'reka-ui'

const props = defineProps<DrawerTitleProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardProps(delegatedProps)
</script>

<template>
  <DrawerTitle data-slot="drawer-title" v-bind="forwarded" :class="['fui-drawer-title', props.class]">
    <slot />
  </DrawerTitle>
</template>
```

`components/ui/drawer/DrawerTrigger.vue`

```vue
<script lang="ts">
/** Opens the drawer. */
export default {}
</script>

<script lang="ts" setup>
import type { DrawerTriggerProps } from 'reka-ui'
import { DrawerTrigger, useForwardProps } from 'reka-ui'

const props = defineProps<DrawerTriggerProps>()
const forwarded = useForwardProps(props)
</script>

<template>
  <DrawerTrigger data-slot="drawer-trigger" v-bind="forwarded">
    <slot />
  </DrawerTrigger>
</template>
```

## API of the items the example uses

### Button

[Full docs](https://ui.frontic.com/components/ui/button.md) · `npx @frontic/ui add button`

#### Button

Lets the user trigger an action, such as adding a product to the cart. Renders a `<button>`.

`data-slot="button"` · `Button.vue`

Styling: `button`, which extends [`action`](https://ui.frontic.com/DESIGN.md#action). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'button'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `variant` | `'solid' \| 'outline' \| 'soft' \| 'ghost' \| 'link'` | `'solid'` | The visual style: a filled, outlined, tinted, background-free or text-link button. |
| `color` | `'neutral' \| 'primary' \| 'destructive' \| 'buy' \| 'checkout' \| 'inverse'` | `'primary'` | The colour role, e.g. primary for the brand colour, buy for add-to-cart, checkout for the checkout step, destructive for removing. |
| `size` | `'md' \| 'xs' \| 'sm' \| 'lg' \| 'icon-xs' \| 'icon-sm' \| 'icon-md' \| 'icon-lg'` | `'md'` | The button's height and padding; the `icon-*` sizes make a square, icon-only button. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-color` | `'neutral' \| 'primary' \| 'destructive' \| 'buy' \| 'checkout' \| 'inverse'` | Carries the `color` prop. |
| `data-size` | `'md' \| 'xs' \| 'sm' \| 'lg' \| 'icon-xs' \| 'icon-sm' \| 'icon-md' \| 'icon-lg'` | Carries the `size` prop. |
| `data-variant` | `'solid' \| 'outline' \| 'soft' \| 'ghost' \| 'link'` | Carries the `variant` prop. |

### Image

[Full docs](https://ui.frontic.com/components/ui/image.md) · `npx @frontic/ui add image`

#### Image

Shows an image with a loading state and a fallback if it fails to load. Renders a `<span>`.

`data-slot="image"` · `Image.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `src` | `string` |  | URL of the image to show. |
| `alt` | `string` |  | Text alternative read by screen readers; leave empty for a purely decorative image. |
| `loadingEffect` | `LoadingEffect`: `'skeleton' \| 'blur' \| 'none'` | `'skeleton'` | What shows until the image has loaded: a pulsing placeholder, the `placeholderSrc` preview blurred, or nothing. Blur without a `placeholderSrc` shows the pulsing placeholder. |
| `fallbackSrc` | `string` |  | URL tried when `src` fails to load; the fallback slot shows when both fail. |
| `placeholderSrc` | `string` |  | URL of a tiny version of the image, e.g. 24px wide, shown blurred while `src` loads when `loadingEffect` is `blur`. |
| `priority` | `boolean` | `false` | Loads the image first, eagerly and at high fetch priority, instead of when it nears the viewport. Set it on the one image that is the page's largest paint, such as a hero or the first gallery image. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `fallback` |  | Replaces the placeholder shown when the image fails to load and no fallback URL loads either. |

#### ImageFallback

Shows a placeholder in place of an image that failed to load. Renders a `<span>`.

`data-slot="image-fallback"` · `ImageFallback.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

### Price

[Full docs](https://ui.frontic.com/components/ui/price.md) · `npx @frontic/ui add price`

#### Price

Shows a price, with the original price and discount percentage when reduced. Renders a `<span>`.

`data-slot="price"` · `Price.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
| `size` | `PriceSize`: `'md' \| 'sm' \| 'lg'` | `'md'` | The text size of the price. |
| `align` | `PriceAlign`: `'start' \| 'end'` | `'start'` | Which side the badge, the prices and the hint line up on. |
| `price` (required) | `PriceType` |  | The price to show: `amount` and `ref` in minor units such as cents, `currency` as an ISO 4217 code. |
| `locale` | `string` |  | The locale that formats the price, e.g. `de-DE`; the ConfigProvider's when left out. |
| `showRef` | `boolean` | `true` | Shows the reference price struck through when it is higher than the amount. |
| `showDiscount` | `boolean` | `true` | Shows the saving as a percentage next to a discounted price. |
| `showHint` | `boolean` | `true` | Shows the hint below the price when the hint slot holds one. |
| `refLabel` | `string` | `'Was'` | Read out before the original price of a reduced product; hidden on screen. |
| `amountLabel` | `string` | `'Now'` | Read out before the current price of a reduced product; hidden on screen. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `hint` |  | A note below the price, such as tax and shipping. |

#### PriceAmount

Shows the current price of the product. Renders a `<span>`.

`data-slot="price-amount"` · `PriceAmount.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

#### PriceRef

Shows the original price of a discounted product. Renders a `<span>`.

`data-slot="price-ref"` · `PriceRef.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

#### PriceDiscount

Shows the percentage saved against the original price. Renders a `<span>`.

`data-slot="price-discount"` · `PriceDiscount.vue`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ percent: number; }` | Replaces the discount badge; gets the percentage saved. |

#### PriceHint

Holds a note below the price, such as tax and shipping, which may link on. Renders a `<span>`.

`data-slot="price-hint"` · `PriceHint.vue`

Styling: `price-hint`, which extends [`link-foreground`](https://ui.frontic.com/DESIGN.md#link-foreground). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'span'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |
