---
title: "Drawer"
description: "A panel that slides in from the edge of the screen, with support for swipe-to-dismiss, snap points, and nested drawers."
canonical_url: "https://vinicunca.dev/akar/components/drawer"
---
# Drawer

> A panel that slides in from the edge of the screen, with support for swipe-to-dismiss, snap points, and nested drawers.

`````vue [index.vue]
<script setup lang="ts">
import {
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerHandle,
  DrawerOverlay,
  DrawerPortal,
  DrawerRoot,
  DrawerTitle,
  DrawerTrigger,
} from 'akar'
import './styles.css'
</script>

<template>
  <div class="akar-demo akar-demo-Drawer">
    <DrawerRoot>
        <DrawerTrigger class="Button grass">
          Open Drawer
        </DrawerTrigger>
        <DrawerPortal>
          <DrawerOverlay class="DrawerOverlay" />
          <DrawerContent class="DrawerContent">
            <DrawerHandle class="DrawerHandle" />
            <div class="DrawerBody">
              <DrawerTitle class="DrawerTitle">
                Edit profile
              </DrawerTitle>
              <DrawerDescription class="DrawerDescription">
                Make changes to your profile here. Swipe down or click close when you're done.
              </DrawerDescription>
              <fieldset class="Fieldset">
                <label
                  class="Label"
                  for="name"
                > Name </label>
                <input
                  id="name"
                  class="Input"
                  value="Pedro Duarte"
                >
              </fieldset>
              <div class="DrawerFooter">
                <DrawerClose as-child>
                  <button class="Button green">
                    Save changes
                  </button>
                </DrawerClose>
              </div>
            </div>
          </DrawerContent>
        </DrawerPortal>
      </DrawerRoot>
  </div>
</template>
```

```css [styles.css]
@import '../_colors/black-alpha.css';
@import '../_colors/green.css';
@import '../_colors/mauve.css';
@import '../_colors/grass.css';

@scope (.akar-demo-Drawer) {
/* reset */
button,
fieldset,
input {
  all: unset;
}

.DrawerOverlay {
  background-color: var(--black-a9);
  position: fixed;
  inset: 0;
  z-index: 30;
}
.DrawerOverlay[data-state='open'] {
  animation: drawerOverlayShow 450ms cubic-bezier(0.32, 0.72, 0, 1);
}
.DrawerOverlay[data-state='closed'] {
  animation: drawerOverlayHide 450ms cubic-bezier(0.32, 0.72, 0, 1);
}

.DrawerContent {
  /* Extra drawer area that hangs below the viewport edge, kept off-screen by an
     equal negative margin. Pulling the drawer away from its edge slides the
     bleed into view instead of exposing the overlay behind it, so the drawer
     stretches rather than visually detaching while it rubber-bands. */
  --bleed: 48px;

  background-color: white;
  border-radius: 16px 16px 0 0;
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  margin-inline: auto;
  margin-bottom: calc(-1 * var(--bleed));
  padding-bottom: calc(env(safe-area-inset-bottom, 0px) + var(--bleed));
  max-width: 500px;
  display: flex;
  flex-direction: column;
  outline: none;
  z-index: 100;
  /* `--drawer-swipe-movement-y` is written by DrawerContent while dragging. */
  transform: translateY(var(--drawer-swipe-movement-y, 0px));
  transition: transform 450ms cubic-bezier(0.32, 0.72, 0, 1);
  will-change: transform;
}
/* Enter/exit keyframes animate the independent `translate` property so they
   compose with the inline `transform` carrying the live drag offset. */
.DrawerContent[data-state='open'] {
  animation: drawerSlideBottomIn 450ms cubic-bezier(0.32, 0.72, 0, 1);
}
.DrawerContent[data-state='closed'] {
  animation: drawerSlideBottomOut 450ms cubic-bezier(0.32, 0.72, 0, 1);
}
/* Silence the transform transition during an active drag so it tracks the
   pointer in real time. */
.DrawerContent[data-swiping] {
  transition-duration: 0ms;
  user-select: none;
}

.DrawerHandle {
  width: 48px;
  height: 6px;
  margin: 12px auto 0;
  flex-shrink: 0;
  border-radius: 9999px;
  background-color: var(--mauve-6);
}

.DrawerBody {
  padding: 24px;
}

.DrawerTitle {
  margin: 0;
  color: var(--mauve-12);
  font-size: 17px;
  font-weight: 600;
}

.DrawerDescription {
  margin: 10px 0 20px;
  color: var(--mauve-11);
  font-size: 15px;
  line-height: 1.5;
}

.DrawerFooter {
  display: flex;
  margin-top: 25px;
  justify-content: flex-end;
}

.Fieldset {
  display: flex;
  gap: 20px;
  align-items: center;
  margin-bottom: 15px;
}

.Label {
  width: 90px;
  text-align: right;
  font-size: 15px;
  color: var(--grass-11);
}

.Input {
  width: 100%;
  flex: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  padding: 0 10px;
  height: 35px;
  font-size: 15px;
  line-height: 1;
  color: var(--grass-11);
  background-color: #fafafa;
  box-shadow: 0 0 0 1px var(--green-7);
}
.Input:focus {
  box-shadow: 0 0 0 2px var(--green-8);
}

.Button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 6px;
  padding: 0 15px;
  font-size: 15px;
  line-height: 1;
  height: 35px;
  font-weight: 600;
  cursor: pointer;
}
.Button.grass {
  background-color: white;
  color: var(--grass-11);
  box-shadow: 0 2px 10px var(--black-a7);
}
.Button.grass:hover {
  background-color: var(--mauve-3);
}
.Button.green {
  background-color: var(--green-4);
  color: var(--green-11);
}
.Button.green:hover {
  background-color: var(--green-5);
}

@keyframes drawerOverlayShow {
  from {
    opacity: 0;
  }
}
@keyframes drawerOverlayHide {
  to {
    opacity: 0;
  }
}
/* The bleed already sits below the viewport edge, so the drawer only has to
   travel `height - bleed` to clear it. Translating a full 100% would overshoot
   by the bleed and make the slide look faster than it is. */
@keyframes drawerSlideBottomIn {
  from {
    translate: 0 calc(100% - var(--bleed));
  }
}
@keyframes drawerSlideBottomOut {
  to {
    translate: 0 calc(100% - var(--bleed));
  }
}
}
```

## Features

- Slides in from any edge — bottom, top, left or right.
- Swipe-to-dismiss with momentum, plus optional swipe-to-open.
- Supports snap points for partially-open resting positions.
- Modal, focus-trapped non-modal, and fully non-modal modes.
- Can be controlled or uncontrolled.
- Manages screen reader announcements with `Title` and `Description` components.
- Esc closes the component automatically.

## Installation

Install the component from your command line.

```bash [pnpm]
pnpm add akar
```

```bash [yarn]
yarn add akar
```

```bash [npm]
npm install akar
```

```bash [bun]
bun add akar
```

## Anatomy

Import all parts and piece them together.

```vue
<script setup>
import {
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerHandle,
  DrawerOverlay,
  DrawerPortal,
  DrawerRoot,
  DrawerTitle,
  DrawerTrigger,
} from 'akar';
</script>

<template>
  <DrawerRoot>
    <DrawerTrigger />
    <DrawerPortal>
      <DrawerOverlay />
      <DrawerContent>
        <DrawerHandle />
        <DrawerTitle />
        <DrawerDescription />
        <DrawerClose />
      </DrawerContent>
    </DrawerPortal>
  </DrawerRoot>
</template>
```

## Animating the drawer

The Drawer is unstyled, so the enter/exit transitions and the swipe gesture are
driven entirely by your CSS. `DrawerContent` exposes the drag offset through CSS
custom properties so you can wire up the live transform:

- `--drawer-swipe-movement-y` — vertical drag offset (for top/bottom drawers)
- `--drawer-swipe-movement-x` — horizontal drag offset (for left/right drawers)
- `--drawer-snap-point-offset` — offset of the active snap point, when snap points are used
- `--drawer-swipe-progress` — `0` at rest, approaching `1` as the drawer is swiped away

```css
.DrawerContent {
  /* Follow the pointer while dragging. */
  transform: translateY(var(--drawer-swipe-movement-y, 0px));
  transition: transform 450ms cubic-bezier(0.32, 0.72, 0, 1);
}

/* Use the independent `translate` property for the enter/exit keyframes so they
   compose with the `transform` above instead of clobbering the drag offset. */
.DrawerContent[data-state='open'] {
  animation: slideIn 450ms cubic-bezier(0.32, 0.72, 0, 1);
}
.DrawerContent[data-state='closed'] {
  animation: slideOut 450ms cubic-bezier(0.32, 0.72, 0, 1);
}

/* Cut the transition while actively dragging so the drawer tracks the pointer. */
.DrawerContent[data-swiping] {
  transition-duration: 0ms;
}

@keyframes slideIn {
  from {
    translate: 0 100%;
  }
}
@keyframes slideOut {
  to {
    translate: 0 100%;
  }
}
```

### Bleeding past the edge

Dragging a drawer *away* from the edge it is anchored to doesn't dismiss it —
the gesture is damped and the drawer rubber-bands back on release. Without extra
room the drawer lifts clean off the edge during that pull and the overlay shows
through the gap.

Give the drawer some **bleed**: extra area past its anchored edge, held
off-screen by an equal negative margin. Pulling the drawer now slides the bleed
into view instead of the overlay, so it reads as a stretch rather than a
detachment.

```css
.DrawerContent {
  --bleed: 48px;
  padding-bottom: calc(env(safe-area-inset-bottom, 0px) + var(--bleed));
  margin-bottom: calc(-1 * var(--bleed));
}

/* The bleed already sits below the viewport edge, so the drawer only has to
   travel `height - bleed` to clear it. */
@keyframes slideIn {
  from {
    translate: 0 calc(100% - var(--bleed));
  }
}
@keyframes slideOut {
  to {
    translate: 0 calc(100% - var(--bleed));
  }
}
```

For a side drawer, swap the axis and point the bleed at the edge the drawer is
anchored to — a right-anchored drawer bleeds right, a left-anchored one bleeds
left:

```css
/* Anchored right */
.DrawerContent {
  padding-inline-end: calc(24px + var(--bleed));
  margin-inline-end: calc(-1 * var(--bleed));
}
@keyframes slideIn {
  from {
    translate: calc(100% - var(--bleed)) 0;
  }
}

/* Anchored left */
.DrawerContent {
  padding-inline-start: calc(24px + var(--bleed));
  margin-inline-start: calc(-1 * var(--bleed));
}
@keyframes slideIn {
  from {
    translate: calc(-100% + var(--bleed)) 0;
  }
}
```

## Soft keyboard

On mobile the software keyboard slides over the bottom of the viewport without
resizing it, so a bottom-anchored drawer — and the field the user just tapped —
end up underneath it.

Wrap the drawer in `DrawerVirtualKeyboardProvider` to make it react. It requires
a `DrawerViewport` around `DrawerContent`: the viewport is the measurement root
and hosts the keyboard variable, which inherits into the popup and everything in
it.

```vue
<template>
  <DrawerRoot>
    <DrawerVirtualKeyboardProvider>
      <DrawerTrigger />
      <DrawerPortal>
        <DrawerOverlay />
        <DrawerViewport>
          <DrawerContent>
            <input>
          </DrawerContent>
        </DrawerViewport>
      </DrawerPortal>
    </DrawerVirtualKeyboardProvider>
  </DrawerRoot>
</template>
```

While a field inside the drawer is focused, the provider scrolls it into the band
left visible above the keyboard and publishes the keyboard's height as
`--drawer-keyboard-inset` on the viewport.

- **Keep the popup frame stable** — put header and footer content outside a plain
scrollable body so only the body moves.
- **Lift a pinned footer** — offset it by `var(--drawer-keyboard-inset, 0px)`,
either as padding or as part of its height.
- **Always include the 0px fallback** — the variable is only set while the
keyboard is aligned, so a bare `var(--drawer-keyboard-inset)` is invalid before
the first alignment and after cleanup.

```css
.DrawerFooter {
  padding-bottom: calc(1rem + var(--drawer-keyboard-inset, 0px));
}
```

To lift the whole sheet instead, give up the keyboard's height so its top stays
on screen:

```css
.DrawerContent {
  height: calc(90dvh - var(--drawer-keyboard-inset, 0px));
  max-height: calc(90dvh - var(--drawer-keyboard-inset, 0px));
}
```

Alignment is suspended while the viewport is pinch-zoomed, and viewport changes
under 60px are treated as browser chrome rather than the keyboard.

## API Reference

### Root

Contains all the parts of a drawer. Manages open state, modality, swipe direction
and snap points, and provides the context consumed by every other part.

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `false` | `boolean` |  |
| `defaultSnapPoint` |  | `null \| string \| number` |  |
| `modal` | `true` | `false \| true \| "trap-focus"` | 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 |
| `open` |  | `boolean` | v-model:open |
| `snapPoint` |  | `null \| string \| number` | v-model:snapPoint |
| `snapPoints` |  | `DrawerSnapPoint[]` | Preset snap positions (fractions 0-1, pixels >1, or '148px'/'30rem' strings) |
| `snapToSequentialPoints` | `false` | `boolean` | When true, snaps to the next sequential snap point (one step at a time).<br>When false, snaps to the nearest snap point by distance. |
| `swipeDirection` | `"down"` | `"right" \| "left" \| "down" \| "up"` | Direction to swipe to dismiss. |

**Events**

| Event | Payload | Description |
| --- | --- | --- |
| `update:open` | `value: boolean, details?: DrawerOpenChangeDetails` |  |
| `update:openComplete` | `value: boolean` |  |
| `update:snapPoint` | `value: DrawerSnapPoint \| null` |  |

**Slots**

| Slot | Payload | Description |
| --- | --- | --- |
| `open` | `boolean` |  |
| `close` | `(): void` |  |

[DrawerRoot.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerRoot.vue)

### Trigger

The button that opens the drawer.

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `as` | `"button"` | `AsTag \| Component` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `asChild` |  | `boolean` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](/akar/guides/composition) guide for more details. |

[DrawerTrigger.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerTrigger.vue)

| Data attribute | Value |
| --- | --- |
| `[data-state]` | "open" \\| "closed" |

### Portal

When used, portals your overlay and content parts into the `body`.

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `defer` |  | `boolean` | Defer the resolving of a Teleport target until other parts of the<br>application have mounted (requires Vue 3.5.0+)<br><br>[reference](https://vuejs.org/guide/built-ins/teleport.html#deferred-teleport) |
| `disabled` |  | `boolean` | Disable teleport and render the component inline<br><br>[reference](https://vuejs.org/guide/built-ins/teleport.html#disabling-teleport) |
| `forceMount` |  | `boolean` | Used to force mounting when more control is needed. Useful when<br>controlling animation with Vue animation libraries. |
| `to` |  | `string \| HTMLElement` | Vue native teleport component prop `:to`<br><br>[reference](https://vuejs.org/guide/built-ins/teleport.html#basic-usage) |

[DrawerPortal.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerPortal.vue)

### Overlay

A layer that covers the inert portion of the view when the drawer is open. Only
rendered when the drawer is `modal`.

> [!NOTE]
> 
> Defer mounting with `forceMount` / [`Presence`](https://vinicunca.dev/akar/utilities/presence). See the [animation guide](https://vinicunca.dev/akar/guides/animation).

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `as` | `"div"` | `AsTag \| Component` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `asChild` |  | `boolean` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](/akar/guides/composition) guide for more details. |
| `forceMount` | `false` | `boolean` | Keep mounted for animation control. |
| `forceRender` | `false` | `boolean` | Render even when inside a nested drawer. |

[DrawerOverlay.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerOverlay.vue)

| Data attribute | Value |
| --- | --- |
| `[data-state]` | "open" \\| "closed" |
| `[data-swipe-direction]` | "up" \\| "down" \\| "left" \\| "right" |
| `[data-swiping]` | "Present when the drawer is being dragged" |

### Content

Contains the content to be rendered in the open drawer. Owns the swipe gesture
and exposes the drag offset through CSS custom properties (see [Animating the
drawer](#animating-the-drawer)). Also aliased as `DrawerPopup` for Base UI parity.

> [!NOTE]
> 
> Defer mounting with `forceMount` / [`Presence`](https://vinicunca.dev/akar/utilities/presence). See the [animation guide](https://vinicunca.dev/akar/guides/animation).

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `as` | `"div"` | `AsTag \| Component` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `asChild` |  | `boolean` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](/akar/guides/composition) guide for more details. |
| `disableOutsidePointerEvents` |  | `boolean` | When `true`, hover/focus/click interactions will be disabled on elements outside<br>the `DismissableLayer`. Users will need to click twice on outside elements to<br>interact with them: once to close the `DismissableLayer`, and again to trigger the element. |
| `finalFocus` | `true` | `boolean \| HTMLElement \| null` | 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 |
| `forceMount` |  | `boolean` |  |
| `initialFocus` | `true` | `boolean \| HTMLElement \| null` | 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 |

**Events**

| Event | Payload | Description |
| --- | --- | --- |
| `closeAutoFocus` | `event: Event` |  |
| `escapeKeyDown` | `event: KeyboardEvent` | Event handler called when the escape key is down.<br>Can be prevented. |
| `focusOutside` | `event: FocusOutsideEvent` | Event handler called when the focus moves outside of the `DismissableLayer`.<br>Can be prevented. |
| `interactOutside` | `event: PointerDownOutsideEvent \| FocusOutsideEvent` | Event handler called when an interaction happens outside the `DismissableLayer`.<br>Specifically, when a `pointerdown` event happens outside or focus moves outside of it.<br>Can be prevented. |
| `openAutoFocus` | `event: Event` |  |
| `pointerDownOutside` | `event: PointerDownOutsideEvent` | Event handler called when a `pointerdown` event happens outside of the `DismissableLayer`.<br>Can be prevented. |

[DrawerContent.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerContent.vue)

| Data attribute | Value |
| --- | --- |
| `[data-state]` | "open" \\| "closed" |
| `[data-swipe-direction]` | "up" \\| "down" \\| "left" \\| "right" |
| `[data-swiping]` | "Present while the drawer is being dragged" |
| `[data-nested-drawer-open]` | "Present when a nested drawer is open" |

### Close

The button that closes the drawer.

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `as` | `"button"` | `AsTag \| Component` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `asChild` |  | `boolean` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](/akar/guides/composition) guide for more details. |

[DrawerClose.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerClose.vue)

### Title

An accessible title to be announced when the drawer is opened.

If you want to hide the title, wrap it inside our Visually Hidden utility like this `<VisuallyHidden asChild>`.

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `as` | `"h2"` | `AsTag \| Component` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `asChild` |  | `boolean` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](/akar/guides/composition) guide for more details. |

[DrawerTitle.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerTitle.vue)

### Description

An optional accessible description to be announced when the drawer is opened.

If you want to hide the description, wrap it inside our Visually Hidden utility like this `<VisuallyHidden asChild>`. If you want to remove the description entirely, remove this part and pass `:aria-describedby="undefined"` to `DrawerContent`.

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `as` | `"p"` | `AsTag \| Component` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `asChild` |  | `boolean` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](/akar/guides/composition) guide for more details. |

[DrawerDescription.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerDescription.vue)

### Handle

A visual grab handle that hints the drawer can be dragged. It is purely
decorative (`aria-hidden`) — the whole content is draggable regardless.

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `as` | `"div"` | `AsTag \| Component` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `asChild` |  | `boolean` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](/akar/guides/composition) guide for more details. |

[DrawerHandle.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerHandle.vue)

| Data attribute | Value |
| --- | --- |
| `[data-state]` | "open" \\| "closed" |

### SwipeArea

An off-screen edge area that lets users swipe the drawer **open**. By default it
listens on the opposite side of the Root's `swipeDirection`.

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `as` | `"div"` | `AsTag \| Component` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `asChild` |  | `boolean` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](/akar/guides/composition) guide for more details. |
| `disabled` | `false` | `boolean` | Disable swipe-to-open. |
| `swipeDirection` |  | `"right" \| "left" \| "down" \| "up"` | Override the open swipe direction (defaults to opposite of Root's swipeDirection). |

[DrawerSwipeArea.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerSwipeArea.vue)

| Data attribute | Value |
| --- | --- |
| `[data-state]` | "open" \\| "closed" |
| `[data-swipe-direction]` | "up" \\| "down" \\| "left" \\| "right" |

### Viewport

An optional wrapper around `DrawerContent`. Mirrors Base UI's `Drawer.Viewport`
and carries a `data-drawer-viewport` attribute for downstream selectors.
Required when using `VirtualKeyboardProvider`, which measures the keyboard
against it.

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `as` | `"div"` | `AsTag \| Component` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `asChild` |  | `boolean` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](/akar/guides/composition) guide for more details. |

[DrawerViewport.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerViewport.vue)

### VirtualKeyboardProvider

Makes the drawer react to the software keyboard: it keeps the focused field
visible and exposes `--drawer-keyboard-inset` on `DrawerViewport`, which it
requires (see [Soft keyboard](#soft-keyboard)). Takes no props and renders no
markup of its own.

### Indent

Wraps page content that should visually shift (scale/indent) as the drawer is
swiped, mimicking the native iOS "card stack" effect. Reads the visual state
from a parent `DrawerProvider` and syncs the `--drawer-swipe-progress` and
`--drawer-height` CSS variables onto its element.

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `as` | `"div"` | `AsTag \| Component` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `asChild` |  | `boolean` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](/akar/guides/composition) guide for more details. |

[DrawerIndent.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerIndent.vue)

| Data attribute | Value |
| --- | --- |
| `[data-active]` | "Present when a drawer is open" |
| `[data-inactive]` | "Present when no drawer is open" |

### IndentBackground

The backdrop layer revealed behind an indented page (typically a solid colour
that peeks out as the page scales down). Companion to `Indent`.

**Props**

| Prop | Default | Type | Description |
| --- | --- | --- | --- |
| `as` | `"div"` | `AsTag \| Component` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `asChild` |  | `boolean` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](/akar/guides/composition) guide for more details. |

[DrawerIndentBackground.vue source](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer/DrawerIndentBackground.vue)

| Data attribute | Value |
| --- | --- |
| `[data-active]` | "Present when a drawer is open" |
| `[data-inactive]` | "Present when no drawer is open" |

## Examples

### Choosing a side

`DrawerRoot` slides up from the bottom by default. Set `swipeDirection` to change
the edge the drawer attaches to and the direction users swipe to dismiss it.

```vue
<template>
  <DrawerRoot swipe-direction="right">
    <DrawerTrigger>Open</DrawerTrigger>
    <DrawerPortal>
      <DrawerOverlay />
      <DrawerContent>...</DrawerContent>
    </DrawerPortal>
  </DrawerRoot>
</template>
```

```css
/* A right-anchored drawer slides along the X axis instead. */
.DrawerContent {
  position: fixed;
  inset-block: 0;
  right: 0;
  width: 20rem;
  transform: translateX(var(--drawer-swipe-movement-x, 0px));
  transition: transform 450ms cubic-bezier(0.32, 0.72, 0, 1);
}
.DrawerContent[data-state='open'] {
  animation: slideInRight 450ms cubic-bezier(0.32, 0.72, 0, 1);
}
.DrawerContent[data-state='closed'] {
  animation: slideOutRight 450ms cubic-bezier(0.32, 0.72, 0, 1);
}
.DrawerContent[data-swiping] {
  transition-duration: 0ms;
}

@keyframes slideInRight {
  from {
    translate: 100% 0;
  }
}
@keyframes slideOutRight {
  to {
    translate: 100% 0;
  }
}
```

### Snap points

Provide `snapPoints` to give the drawer intermediate resting positions. Each point
is a fraction (`0`–`1`), a pixel value (`> 1`), or a string like `'148px'` /
`'30rem'`. Use `v-model:snapPoint` to read or control the active one.

```vue
<script setup>
import { DrawerContent, DrawerOverlay, DrawerPortal, DrawerRoot, DrawerTrigger } from 'akar'
import { ref } from 'vue'

const snapPoints = [0.4, 0.75, 1]
const activeSnapPoint = ref(0.4)
</script>

<template>
  <DrawerRoot
    v-model:snap-point="activeSnapPoint"
    :snap-points="snapPoints"
  >
    <DrawerTrigger>Open</DrawerTrigger>
    <DrawerPortal>
      <DrawerOverlay />
      <DrawerContent>...</DrawerContent>
    </DrawerPortal>
  </DrawerRoot>
</template>
```

### Non-modal drawer

By default the drawer is `modal`: it traps focus, locks scroll, and dismisses on
outside press. Set `modal` to `'trap-focus'` to keep the focus trap while still
allowing interaction with the rest of the page (no overlay is rendered), or to
`false` for a fully non-modal panel.

```vue
<template>
  <DrawerRoot :modal="false">
    <DrawerTrigger>Open</DrawerTrigger>
    <DrawerPortal>
      <DrawerContent>...</DrawerContent>
    </DrawerPortal>
  </DrawerRoot>
</template>
```

### Reacting to why the drawer closed

The `update:open` event carries a `details` object whose `reason` tells you what
triggered the change — useful for distinguishing a deliberate close from a swipe.

```vue
<script setup>
import { DrawerContent, DrawerOverlay, DrawerPortal, DrawerRoot, DrawerTrigger } from 'akar'

function onOpenChange(open, details) {
  if (!open && details?.reason === 'swipe') {
    // user flicked the drawer away
  }
}
</script>

<template>
  <DrawerRoot @update:open="onOpenChange">
    <DrawerTrigger>Open</DrawerTrigger>
    <DrawerPortal>
      <DrawerOverlay />
      <DrawerContent>...</DrawerContent>
    </DrawerPortal>
  </DrawerRoot>
</template>
```

Possible reasons are `swipe`, `escape-key`, `outside-press`, `click`, `cancel`,
`trigger-press` and `close-press`.

### Close using slot props

`DrawerRoot` exposes `open` and `close` via its default slot, so you can close the
drawer programmatically from anywhere inside it.

```vue
<template>
  <DrawerRoot v-slot="{ close }">
    <DrawerTrigger>Open</DrawerTrigger>
    <DrawerPortal>
      <DrawerOverlay />
      <DrawerContent>
        <form @submit.prevent="close">
          <!-- some inputs -->
          <button type="submit">
            Save
          </button>
        </form>
      </DrawerContent>
    </DrawerPortal>
  </DrawerRoot>
</template>
```

## Accessibility

Adheres to the [Dialog WAI-ARIA design pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/).

### Keyboard Interactions

| Key | Description |
| --- | --- |
| <kbd>Space</kbd> | Opens/closes the drawer. |
| <kbd>Enter</kbd> | Opens/closes the drawer. |
| <kbd>Tab</kbd> | Moves focus to the next focusable element. |
| <kbd>Shift + Tab</kbd> | Moves focus to the previous focusable element. |
| <kbd>Esc</kbd> | Closes the drawer and moves focus to `DrawerTrigger`. |

## Custom APIs

Create your own API by abstracting the primitive parts into your own component.

### Abstract the overlay and the portal

This example abstracts the `DrawerPortal` and `DrawerOverlay` parts.

#### Usage

```vue
<script setup>
import { Drawer, DrawerContent, DrawerTrigger } from './your-drawer';
</script>

<template>
  <Drawer>
    <DrawerTrigger>Drawer trigger</DrawerTrigger>
    <DrawerContent>Drawer Content</DrawerContent>
  </Drawer>
</template>
```

#### Implementation

```ts
export { DrawerRoot as Drawer, DrawerTrigger } from 'akar';
// your-drawer.ts
export { default as DrawerContent } from 'DrawerContent.vue';
```

```vue
<!-- DrawerContent.vue -->
<script setup lang="ts">
import type { DrawerContentEmits, DrawerContentProps } from 'akar';
import { DrawerContent, DrawerHandle, DrawerOverlay, DrawerPortal, useForwardPropsEmits } from 'akar';

const props = defineProps<DrawerContentProps>();
const emits = defineEmits<DrawerContentEmits>();

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

<template>
  <DrawerPortal>
    <DrawerOverlay />
    <DrawerContent v-bind="forwarded">
      <DrawerHandle />
      <slot />
    </DrawerContent>
  </DrawerPortal>
</template>
```

---

- [ARIA](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
- [GitHub](https://github.com/vinicunca/akar/blob/v2.0.20/packages/core/src/Drawer)
- [npm](https://www.npmjs.com/package/akar)


## Sitemap

See the full [sitemap](https://vinicunca.dev/sitemap.md) for all pages.
