# Drawer (/docs/drawer)





The `Drawer` is an edge-anchored panel built on Ark UI's `Drawer` state machine — so it ships swipe-to-dismiss, snap points and a drag grabber out of the box. The kit layers on an &#x2A;*RTL-aware `placement`** prop (Ark has none): docking, sizing and the slide animation are pure CSS keyed on `data-placement`, so `start`/`end` flip automatically under `dir="rtl"`.

```tsx
import { Drawer } from '@42-central/ui-react/drawer';

<Drawer open={open} onOpenChange={setOpen} placement="end" title="Filters">
  <Text>Slides in from the inline-end edge.</Text>
</Drawer>
```

## Placement [#placement]

`placement` accepts the logical edges `start` / `end` / `top` / `bottom`. `start` and `end` resolve against text direction.

<Preview align="center" code="&#x22;<Drawer placement=\&#x22;start\&#x22; … />\n<Drawer placement=\&#x22;end\&#x22; … />   // default\n<Drawer placement=\&#x22;top\&#x22; … />\n<Drawer placement=\&#x22;bottom\&#x22; … />&#x22;">
  <DrawerPlacements />
</Preview>

## Sizes [#sizes]

Inline placements (`start` / `end`) size on width; block placements (`top` / `bottom`) size on height. Both read the `size` token (`xs`–`xl`).

<Preview align="center" code="&#x22;<Drawer placement=\&#x22;end\&#x22; size=\&#x22;sm\&#x22; … />\n<Drawer placement=\&#x22;end\&#x22; size=\&#x22;lg\&#x22; … />&#x22;">
  <div className="flex flex-wrap items-center gap-2">
    <DrawerExample label="sm" placement="end" size="sm" title="Small" />

    <DrawerExample label="md" placement="end" size="md" title="Medium" />

    <DrawerExample label="lg" placement="end" size="lg" title="Large" />
  </div>
</Preview>

## Grabber [#grabber]

Set `withGrabber` to show a draggable handle — useful for `bottom` sheets on touch devices, where the whole panel is swipe-dismissable.

<Preview align="center" code="`<Drawer placement=&#x22;bottom&#x22; withGrabber title=&#x22;Quick actions&#x22; … />`">
  <DrawerExample label="Bottom sheet" placement="bottom" title="Quick actions" />
</Preview>

## Composable parts [#composable-parts]

Like `Modal`, the high-level `<Drawer>` is backed by parts for full control:

```tsx
<Drawer.Root open={open} onOpenChange={({ open }) => setOpen(open)} swipeDirection="end">
  <Drawer.Backdrop />
  <Drawer.Positioner data-placement="end">
    <Drawer.Content data-placement="end">
      <Drawer.Header>
        <Drawer.Title>Filters</Drawer.Title>
        <Drawer.CloseTrigger />
      </Drawer.Header>
      <Drawer.Body>…</Drawer.Body>
    </Drawer.Content>
  </Drawer.Positioner>
</Drawer.Root>
```

`Drawer.Root`, `Drawer.Trigger`, and `Drawer.Context` are raw Ark re-exports with no DOM node (or styling) of their own, so none take a `testId`. `Drawer.Backdrop`, `Drawer.Title`, `Drawer.Description`, `Drawer.Grabber`, `Drawer.Header`, `Drawer.Body`, and `Drawer.Footer` each take their own `testId`, landing on their own rendered element. `Drawer.CloseTrigger`'s `testId` routes to the actual rendered close button — an inner `ActionIcon`, since `CloseTrigger` itself is `asChild` over it — the same pattern as [`Modal.CloseTrigger`](/docs/modal). See [Testing](/docs/testing).

## API [#api]

<TypeTable
  type="{
  open: {
    type: 'boolean',
    description: 'Controlled open state.',
  },
  onOpenChange: {
    type: '(open: boolean) => void',
    description: &#x22;Fires with the next open state. Flattened from Ark's { open }.&#x22;,
  },
  onClose: {
    type: '() => void',
    description: 'Fires on close (any source) — Escape, backdrop click, swipe, or a controlled onOpenChange.',
  },
  placement: {
    type: &#x22;'start' | 'end' | 'top' | 'bottom'&#x22;,
    default: &#x22;'end'&#x22;,
    description: 'RTL-aware docking edge.',
  },
  size: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl'&#x22;,
    default: &#x22;'md'&#x22;,
    description: 'Width (inline placements) or height (block placements).',
  },
  title: {
    type: 'ReactNode',
    description: 'Header title.',
  },
  description: {
    type: 'ReactNode',
    description: 'Optional subtitle under the title.',
  },
  withCloseButton: {
    type: 'boolean',
    default: 'true',
    description: 'Render the default header close button.',
  },
  translations: {
    type: '{ closeTriggerLabel?: string }',
    description: &#x22;Accessible name of the default close button (default \&#x22;Close\&#x22;). The compound CloseTrigger part takes a plain aria-label instead.&#x22;,
  },
  withGrabber: {
    type: 'boolean',
    default: 'false',
    description: 'Show the drag grabber handle.',
  },
  padding: {
    type: &#x22;'xs' | 'sm' | 'md' | 'lg' | 'xl'&#x22;,
    default: &#x22;'md'&#x22;,
    description: 'Inner padding token; sets --drawer-px.',
  },
  closeOnEscape: {
    type: 'boolean',
    default: 'true',
    description: 'Close when Escape is pressed.',
  },
  closeOnInteractOutside: {
    type: 'boolean',
    default: 'true',
    description: 'Close when the backdrop / outside is clicked.',
  },
  testId: {
    type: 'string',
    description: 'Renders as `data-testid` on the real rendered dialog content surface, since that\'s what the top-level `<Drawer>` shorthand renders as its content. See [Testing](/docs/testing).',
  },
}"
/>

All other props (`snapPoints`, `closeThreshold`, `modal`, `trapFocus`, …) forward to Ark's `Drawer.Root`.

## Accessibility [#accessibility]

* Same focus-trap, scroll-lock and focus-restore guarantees as `Modal` — Ark's Drawer machine is a dialog under the hood.
* `placement` uses logical edges and CSS logical properties, so the panel and its slide animation mirror correctly under `dir="rtl"`.
* Swipe-to-dismiss is a progressive enhancement; keyboard and click paths work without it.
