# Drawer (/ui/components/react/drawer)



<Playground component="drawer" />

## Usage [#usage]

```tsx
import {
  Drawer,
  DrawerTrigger,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerDescription,
  DrawerBody,
  DrawerFooter,
  DrawerClose,
} from '@appica/ui-react/drawer'
import { Button } from '@appica/ui-react/button'
```

```tsx
<Drawer side="right">
  <DrawerTrigger render={<Button variant="outline">Open drawer</Button>} />
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Notifications</DrawerTitle>
      <DrawerDescription>Choose what you want to be notified about.</DrawerDescription>
    </DrawerHeader>
    <DrawerBody>{/* content */}</DrawerBody>
    <DrawerFooter>
      <DrawerClose render={<Button>Save</Button>} />
      <DrawerClose render={<Button variant="outline">Cancel</Button>} />
    </DrawerFooter>
  </DrawerContent>
</Drawer>
```

`Drawer` is a compound component built on [Base UI's Drawer](https://base-ui.com/react/components/drawer). It's a [`Dialog`](/ui/components/react/dialog) that **slides in from an edge** and can be **swiped to dismiss** — at home as a mobile bottom sheet or a desktop side panel. Pick the edge with `side`; the swipe direction follows it automatically. By default it's modal and dismissible, with a drag handle and a corner close button:

* **`DrawerTrigger`** — the button that opens the drawer. Use `render` to project it onto your own [`Button`](/ui/components/react/button) (or any element).
* **`DrawerContent`** — the portalled panel. It draws the card, backdrop, handle, focus trap, and corner close button, and accepts the shared **modal props** below.
* **`DrawerHeader`** — groups the title and description with consistent spacing.
* **`DrawerTitle`** — an `<h2>` that labels the panel (`aria-labelledby`).
* **`DrawerDescription`** — a `<p>` that describes it (`aria-describedby`).
* **`DrawerBody`** — the main content region; it owns the scroll and coordinates with the swipe gesture so scrolling doesn't dismiss the drawer.
* **`DrawerFooter`** — the actions, stacked at the bottom of the panel.
* **`DrawerClose`** — a button that dismisses the drawer; project it onto your own [`Button`](/ui/components/react/button) with `render`.

Reach for a `Drawer` when the surface should feel anchored to an edge or be swipeable on touch — navigation, filters, a detail panel, a mobile sheet. For a centered modal, use a [`Dialog`](/ui/components/react/dialog); for a high-stakes confirmation, use an [`Alert Dialog`](/ui/components/react/alert-dialog).

## Examples [#examples]

### Sides [#sides]

Set `side` to `top`, `right`, `bottom`, or `left` to choose the edge the drawer slides in from; the swipe-to-dismiss direction follows. Left and right read as side panels; top and bottom as sheets — cap the sheet's width (here `mx-auto max-w-2xl`) so it doesn't stretch edge-to-edge on a wide screen.

```tsx
import {
  Drawer,
  DrawerTrigger,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerDescription,
  DrawerBody,
  DrawerFooter,
  DrawerClose,
} from '@appica/ui-react/drawer'
import { Button } from '@appica/ui-react/button'

const SIDES = [
  { side: 'top', label: 'Top' },
  { side: 'right', label: 'Right' },
  { side: 'bottom', label: 'Bottom' },
  { side: 'left', label: 'Left' },
] as const

export default function DrawerSides() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-3">
      {SIDES.map(({ side, label }) => {
        const wide = side === 'top' || side === 'bottom'
        return (
          <Drawer key={side} side={side}>
            <DrawerTrigger render={<Button variant="outline">{label}</Button>} />
            <DrawerContent className={wide ? 'mx-auto max-w-2xl' : undefined}>
              <DrawerHeader>
                <DrawerTitle>{label} drawer</DrawerTitle>
                <DrawerDescription>This panel slides in from the {side} edge.</DrawerDescription>
              </DrawerHeader>
              <DrawerBody>
                <p>Drag the handle or swipe toward the {side} edge to dismiss it.</p>
              </DrawerBody>
              <DrawerFooter>
                <DrawerClose render={<Button variant="outline">Close</Button>} />
              </DrawerFooter>
            </DrawerContent>
          </Drawer>
        )
      })}
    </div>
  )
}
```

### Snap points [#snap-points]

Pass `snapPoints` to let a top or bottom drawer rest at intermediate heights. Each value is a fraction of the viewport (`0`–`1`), a pixel number (`> 1`), or a CSS length string (`'24rem'`). The drawer opens at the first point and the user can drag — or flick — between them; it dismisses past the smallest. Great for a bottom sheet that peeks, then expands to full height.

```tsx
import {
  Drawer,
  DrawerTrigger,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerDescription,
} from '@appica/ui-react/drawer'
import { Button } from '@appica/ui-react/button'
import { ScrollArea } from '@appica/ui-react/scroll-area'
import { Thumbnail } from '@appica/ui-react/thumbnail'
import { Music } from '@appica/icons-react'

const TRACKS = [
  { title: 'Nightcall', artist: 'Neon Drift' },
  { title: 'Midnight City', artist: 'Violet Skies' },
  { title: 'Resonance', artist: 'Echo Theory' },
  { title: 'A Real Hero', artist: 'Pixel Horizon' },
  { title: 'Sunset Avenue', artist: 'The Lateliners' },
  { title: 'Turbo Killer', artist: 'Chrome Wolf' },
  { title: 'Afterglow', artist: 'Static Bloom' },
  { title: 'Le Castle', artist: 'Power Circuit' },
  { title: 'Tech Noir', artist: 'Ghost Signal' },
  { title: 'Crystals', artist: 'Cassette 84' },
]

export default function DrawerSnapPoints() {
  return (
    <Drawer snapPoints={[0.4, 1]}>
      <DrawerTrigger render={<Button variant="outline">Open player</Button>} />
      <DrawerContent className="mx-auto max-w-2xl">
        <DrawerHeader>
          <DrawerTitle>Up next</DrawerTitle>
          <DrawerDescription>Drag the sheet to a snap point — peek at 40% or pull it to full height.</DrawerDescription>
        </DrawerHeader>
        <ScrollArea className="min-h-0 flex-1" scrollShadow>
          <div className="flex flex-col gap-5 px-6">
            {TRACKS.map((track) => (
              <div key={track.title} className="flex items-center gap-3">
                <Thumbnail variant="icon-soft" size="sm">
                  <Music />
                </Thumbnail>
                <span className="flex min-w-0 flex-col">
                  <span className="text-foreground-intense truncate text-sm font-medium">{track.title}</span>
                  <span className="text-foreground-muted truncate text-xs">{track.artist}</span>
                </span>
              </div>
            ))}
          </div>
        </ScrollArea>
      </DrawerContent>
    </Drawer>
  )
}
```

### Scrollable content [#scrollable-content]

For content taller than the panel, let a [`ScrollArea`](/ui/components/react/scroll-area) own the overflow between a pinned header and footer. A `left`/`right` drawer is the natural fit: its swipe-to-dismiss is horizontal, so it never fights the vertical scroll.

```tsx
import {
  Drawer,
  DrawerTrigger,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerDescription,
  DrawerFooter,
  DrawerClose,
} from '@appica/ui-react/drawer'
import { Button } from '@appica/ui-react/button'
import { ScrollArea } from '@appica/ui-react/scroll-area'

const EVENTS = [
  { who: 'Sarah Jenkins', what: 'merged pull request #482', when: '2m ago' },
  { who: 'Maya Torres', what: 'commented on “Release checklist”', when: '18m ago' },
  { who: 'Leo Martin', what: 'approved pull request #480', when: '40m ago' },
  { who: 'Daniel Reyes', what: 'closed issue #311', when: '1h ago' },
  { who: 'Priya Shah', what: 'added a label to issue #309', when: '2h ago' },
  { who: 'Emma Clarke', what: 'deployed to production', when: '3h ago' },
  { who: 'Sarah Jenkins', what: 'opened pull request #485', when: '5h ago' },
  { who: 'Tom Becker', what: 'pushed 12 commits to main', when: '6h ago' },
  { who: 'Maya Torres', what: 'requested changes on #478', when: '8h ago' },
  { who: 'Leo Martin', what: 'linked issue #312 to #485', when: '11h ago' },
  { who: 'Maya Torres', what: 'invited 2 members', when: 'Yesterday' },
  { who: 'Nina Foster', what: 'updated the roadmap', when: 'Yesterday' },
  { who: 'Priya Shah', what: 'merged pull request #476', when: 'Yesterday' },
  { who: 'Daniel Reyes', what: 'renamed the project', when: '2d ago' },
  { who: 'Emma Clarke', what: 'archived 4 tasks', when: '2d ago' },
  { who: 'Tom Becker', what: 'reverted commit 9f3c1a', when: '3d ago' },
  { who: 'Sarah Jenkins', what: 'created milestone “v2.0”', when: '3d ago' },
  { who: 'Leo Martin', what: 'closed issue #298', when: '4d ago' },
  { who: 'Nina Foster', what: 'edited the README', when: '5d ago' },
  { who: 'Priya Shah', what: 'opened pull request #470', when: '5d ago' },
  { who: 'Daniel Reyes', what: 'added 3 members', when: '6d ago' },
  { who: 'Emma Clarke', what: 'set up continuous integration', when: '1w ago' },
  { who: 'Tom Becker', what: 'imported the codebase', when: '1w ago' },
  { who: 'Sarah Jenkins', what: 'created the project', when: '2w ago' },
]

export default function DrawerScrollable() {
  return (
    <Drawer side="right">
      <DrawerTrigger render={<Button variant="outline">View activity</Button>} />
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Activity</DrawerTitle>
          <DrawerDescription>Everything that happened in this project.</DrawerDescription>
        </DrawerHeader>
        <ScrollArea className="min-h-0 flex-1">
          <ol className="flex flex-col gap-4 px-6 pb-2">
            {EVENTS.map((event, i) => (
              <li key={i} className="flex flex-col gap-0.5">
                <p className="text-sm">
                  <span className="text-foreground-intense font-medium">{event.who}</span> {event.what}
                </p>
                <p className="text-foreground-muted text-xs">{event.when}</p>
              </li>
            ))}
          </ol>
        </ScrollArea>
        <DrawerFooter>
          <DrawerClose render={<Button variant="outline">Close</Button>} />
        </DrawerFooter>
      </DrawerContent>
    </Drawer>
  )
}
```

### Nested drawers [#nested-drawers]

A drawer can open another drawer. Base UI keeps the parent mounted and tucks it back as the child slides in, so the stack reads as depth. The child's backdrop is suppressed automatically — just nest the markup, each drawer manages its own state.

```tsx
import {
  Drawer,
  DrawerTrigger,
  DrawerContent,
  DrawerHeader,
  DrawerTitle,
  DrawerDescription,
  DrawerBody,
  DrawerFooter,
  DrawerClose,
} from '@appica/ui-react/drawer'
import { Button } from '@appica/ui-react/button'
import { Field, FieldLabel } from '@appica/ui-react/field'
import { Input } from '@appica/ui-react/input'
import { Pencil } from '@appica/icons-react'

export default function DrawerNested() {
  return (
    <Drawer side="right">
      <DrawerTrigger render={<Button variant="outline">Workspace</Button>} />
      <DrawerContent>
        <DrawerHeader>
          <DrawerTitle>Workspace</DrawerTitle>
          <DrawerDescription>Manage the workspace and its members.</DrawerDescription>
        </DrawerHeader>
        <DrawerBody className="flex flex-col items-start gap-3">
          <p>Open a sub-panel to rename the workspace — the parent tucks back to show the stack.</p>
          <Drawer side="right">
            <DrawerTrigger
              render={
                <Button>
                  <Pencil data-icon="start" />
                  Rename workspace
                </Button>
              }
            />
            <DrawerContent closeButton={false}>
              <DrawerHeader>
                <DrawerTitle>Rename workspace</DrawerTitle>
                <DrawerDescription>This name is visible to everyone on the team.</DrawerDescription>
              </DrawerHeader>
              <DrawerBody>
                <Field>
                  <FieldLabel>Workspace name</FieldLabel>
                  <Input defaultValue="Appica" />
                </Field>
              </DrawerBody>
              <DrawerFooter>
                <DrawerClose render={<Button>Save</Button>} />
                <DrawerClose render={<Button variant="outline">Cancel</Button>} />
              </DrawerFooter>
            </DrawerContent>
          </Drawer>
        </DrawerBody>
      </DrawerContent>
    </Drawer>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

The header, body, and footer flip to read right-to-left, and the handle and corner close button move to mirror. The panel portals to `<body>`, so it follows the **document** direction — when you scope RTL to a subtree of an LTR page, set `dir="rtl"` on `DrawerContent` so the panel mirrors too. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="drawer" />

## API reference [#api-reference]

`Drawer` wraps [Base UI's Drawer](https://base-ui.com/react/components/drawer). Each part forwards every remaining prop to the matching Base UI primitive, so anything it accepts works here — the tables below list the Appica additions and the most common props. See the Base UI docs for the complete API.

### Drawer [#drawer]

The root. Holds the open state and the chosen edge; renders no DOM of its own. The swipe direction is derived from `side`, so you don't set `swipeDirection` yourself.

| Prop                      | <ColMinWidth width="200">Type</ColMinWidth>         | Default    | <ColMinWidth width="210">Description</ColMinWidth>                 |
| ------------------------- | --------------------------------------------------- | ---------- | ------------------------------------------------------------------ |
| `side`                    | <Code>'top' \| 'right' \| 'bottom' \| 'left'</Code> | `'bottom'` | The edge the drawer slides in from.                                |
| `open`                    | `boolean`                                           | —          | Controlled open state. Pair with `onOpenChange`.                   |
| `defaultOpen`             | `boolean`                                           | `false`    | Uncontrolled initial open state.                                   |
| `onOpenChange`            | <Code>(open, eventDetails) => void</Code>           | —          | Fires when the drawer opens or closes.                             |
| `modal`                   | <Code>boolean \| 'trap-focus'</Code>                | `true`     | Trap focus and block scroll/interaction with the page behind.      |
| `disablePointerDismissal` | `boolean`                                           | `false`    | Keep the drawer open when the backdrop is clicked.                 |
| `snapPoints`              | `(number \| string)[]`                              | —          | Resting heights for a top/bottom drawer (fraction, px, or length). |
| `defaultSnapPoint`        | `number \| string \| null`                          | —          | Uncontrolled initial snap point (defaults to the first).           |
| `snapPoint`               | `number \| string \| null`                          | —          | Controlled active snap point. Pair with `onSnapPointChange`.       |
| `onSnapPointChange`       | `(point) => void`                                   | —          | Fires when the active snap point changes.                          |
| `onOpenChangeComplete`    | `(open) => void`                                    | —          | Fires after the open/close transition finishes.                    |
| `actionsRef`              | `RefObject`                                         | —          | Imperative handle to `close()` / `unmount()` the panel.            |

### DrawerTrigger [#drawertrigger]

The button that opens the drawer. Forwards Base UI's `Drawer.Trigger`; use `render` to project it onto your own element.

| Prop           | <ColMinWidth width="220">Type</ColMinWidth>                 | Default | Description                                          |
| -------------- | ----------------------------------------------------------- | ------- | ---------------------------------------------------- |
| `render`       | <Code>ReactElement \| (props, state) => ReactElement</Code> | —       | Render the trigger as your own element (a `Button`). |
| `nativeButton` | `boolean`                                                   | `true`  | Whether the rendered element is a native `<button>`. |
| `className`    | `string`                                                    | —       | Extra classes, merged via `tailwind-merge`.          |

### DrawerContent [#drawercontent]

The portalled panel. Draws the card, backdrop, drag handle, focus trap, and corner close button, and maps the underlying Base UI Portal / Backdrop / Viewport so you don't have to reach into them. Shared with [`Dialog`](/ui/components/react/dialog) and [`Alert Dialog`](/ui/components/react/alert-dialog) through the **modal props**.

| Prop            | <ColMinWidth width="170">Type</ColMinWidth> | Default   | Description                                                                       |
| --------------- | ------------------------------------------- | --------- | --------------------------------------------------------------------------------- |
| `closeButton`   | `boolean`                                   | `true`    | Render the × button in the corner.                                                |
| `closeLabel`    | `string`                                    | `'Close'` | Accessible label for the close button.                                            |
| `backdrop`      | `boolean`                                   | —         | Render the dimmed backdrop. Defaults to `true` at the top level, off when nested. |
| `container`     | <Code>HTMLElement \| ref \| null</Code>     | —         | Portal target — render the panel into a different node.                           |
| `keepMounted`   | `boolean`                                   | `false`   | Keep the panel mounted in the DOM while closed.                                   |
| `backdropProps` | `object`                                    | —         | Escape hatch for the backdrop element (`className`, `style`, `forceRender`, …).   |
| `viewportProps` | `object`                                    | —         | Escape hatch for the viewport element that positions the panel.                   |
| `portalProps`   | `object`                                    | —         | Escape hatch for the portal element.                                              |
| `className`     | `string`                                    | —         | Extra classes on the panel (e.g. a `max-w-*` cap or a custom `w-*`).              |

### DrawerHeader [#drawerheader]

A layout wrapper that groups the title and description with consistent padding and spacing.

| Prop        | Type        | Default | Description                                 |
| ----------- | ----------- | ------- | ------------------------------------------- |
| `children`  | `ReactNode` | —       | Typically the title and description.        |
| `className` | `string`    | —       | Extra classes, merged via `tailwind-merge`. |

### DrawerTitle [#drawertitle]

An `<h2>` that labels the panel. Rendering one sets the panel's `aria-labelledby`.

| Prop        | Type        | Default | Description                                 |
| ----------- | ----------- | ------- | ------------------------------------------- |
| `children`  | `ReactNode` | —       | The title text.                             |
| `className` | `string`    | —       | Extra classes, merged via `tailwind-merge`. |

### DrawerDescription [#drawerdescription]

A `<p>` that describes the panel. Rendering one sets the panel's `aria-describedby`.

| Prop        | Type        | Default | Description                                 |
| ----------- | ----------- | ------- | ------------------------------------------- |
| `children`  | `ReactNode` | —       | The description text.                       |
| `className` | `string`    | —       | Extra classes, merged via `tailwind-merge`. |

### DrawerBody [#drawerbody]

The main content region. Owns the scroll for overflowing content and coordinates with the swipe gesture so scrolling away from the edge doesn't dismiss the drawer.

| Prop        | Type        | Default | Description                                 |
| ----------- | ----------- | ------- | ------------------------------------------- |
| `children`  | `ReactNode` | —       | The body content.                           |
| `className` | `string`    | —       | Extra classes, merged via `tailwind-merge`. |

### DrawerFooter [#drawerfooter]

A layout wrapper for the actions, stacked at the bottom of the panel.

| Prop        | Type        | Default | Description                                 |
| ----------- | ----------- | ------- | ------------------------------------------- |
| `children`  | `ReactNode` | —       | The action buttons.                         |
| `className` | `string`    | —       | Extra classes, merged via `tailwind-merge`. |

### DrawerClose [#drawerclose]

A button that closes the drawer. Forwards Base UI's `Drawer.Close`; project it onto your own element with `render`.

| Prop           | <ColMinWidth width="220">Type</ColMinWidth>                 | Default | Description                                          |
| -------------- | ----------------------------------------------------------- | ------- | ---------------------------------------------------- |
| `render`       | <Code>ReactElement \| (props, state) => ReactElement</Code> | —       | Render the close control as your own element.        |
| `nativeButton` | `boolean`                                                   | `true`  | Whether the rendered element is a native `<button>`. |
| `className`    | `string`                                                    | —       | Extra classes, merged via `tailwind-merge`.          |

### Indenting the page [#indenting-the-page]

Three optional parts — all from `@appica/ui-react/drawer` — produce the effect where the page scales back as a drawer opens. Wrap your app once at the root; most drawers never touch them.

| Part                     | <ColMinWidth width="260">Description</ColMinWidth>                                                                                                      |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DrawerProvider`         | Wraps your app. Tracks every `Drawer` mounted inside it and activates the indent parts while one is open.                                               |
| `DrawerIndent`           | Wraps the page content that scales back. Place it at the top of the tree, next to `DrawerIndentBackground`.                                             |
| `DrawerIndentBackground` | The surface revealed behind the scaled page. Defaults to the body background (`bg-background`); give it a darker color for a contrasting scaled-canvas. |

```tsx
<DrawerProvider>
  <DrawerIndentBackground />
  <DrawerIndent>{/* your app — including any Drawer */}</DrawerIndent>
</DrawerProvider>
```

## Accessibility [#accessibility]

* The panel has `role="dialog"` with `aria-modal`, and `aria-labelledby` / `aria-describedby` wired automatically from `DrawerTitle` and `DrawerDescription` — always render a title.
* Opening **traps focus** inside the panel; **Escape**, an outside click, or a swipe toward the edge close it (set `disablePointerDismissal` to keep it open on a backdrop click), and focus returns to the trigger.
* With `modal` (the default), scroll and interaction with the rest of the page are blocked while the drawer is open. Set `modal={false}` to leave the page interactive.
* The corner close button carries an accessible label (`closeLabel`, default `"Close"`); set it when your drawer needs more specific wording.
* Swipe-to-dismiss, snap transitions, and enter/exit animations are skipped when the user prefers reduced motion.
