apx-dsv0.1Local renderer · live
K
Foundations4
  • Getting started
  • Theming
  • Templates
  • Icons
  • Avatar
  • Badge
  • DataGrid
  • Scheduler
  • Stat
  • Table
  • Timeline
  • TreeView
  • Accordion
  • Alert
  • EmptyState
  • Progress
  • Skeleton
  • Spinner
  • SplashScreen
  • Toast
  • ColorPicker
  • FileUpload
  • Form
  • Rating
  • TagsInput
  • Combobox
  • Field
  • Select
  • Toggle
  • Button
  • Calendar
  • Checkbox
  • DatePicker
  • Input
  • NumberInput
  • Radio
  • Slider
  • Switch
  • Textarea
  • AppShell
  • Div
  • Divider
  • Sidebar
  • Stack
  • Typography
  • Image
  • Breadcrumbs
  • Carousel
  • NavigationMenu
  • Pagination
  • Stepper
  • Tabs
  • Toolbar
  • CommandPalette
  • Confirm
  • Drawer
  • HoverCard
  • Menu
  • Modal
  • Popover
  • Tooltip
  • Icon
  • Card
  • PricingCard
60 componentsapx-ds/renderer
Drawer
Variant↳ other

Overlays

Drawer

Edge-anchored sliding panel. Modal\

Overview

<Drawer /> is the canonical edge-anchored sliding panel primitive — Modal's cousin. Where Modal anchors at the viewport center, Drawer anchors at one of four edges (left / right / top / bottom) and slides in from there. Everything else is reused verbatim from Modal:

  • The same compound API (Drawer.Trigger / Drawer.Content / Drawer.Header / Drawer.Body / Drawer.Footer / Drawer.Close).
  • The same lifecycle hooks (useEscapeStack, useFocusTrap, useScrollLock, <Portal>).
  • The same accessibility shape (role="dialog", aria-modal="true", aria-labelledby, aria-describedby).
  • The same controlled / uncontrolled state model.

What's different lives on <Drawer.Content>: a side prop instead of placement, and a direction-aware size axis (width on horizontal sides, height on vertical sides).

Overview — trigger, header, body, and footer

Loading preview…
Overview.tsx

When to use

  • Mobile navigation menus (side="left", size="sm").
  • Filter / inspector panels next to the main content (side="right").
  • Bottom sheets — share / select / sort actions on mobile (side="bottom").
  • Announcement banners that warrant a dismissable overlay (side="top").
  • Anywhere you'd reach for a Modal but the content is page-related, not page-blocking.

When NOT to use

  • The content is purely a hint with no interactivity → <Tooltip />.
  • The content is a small interactive panel anchored to a trigger → <Popover />.
  • The content is a destructive confirmation centered on the page → <Modal />.
  • The content is a list of actions on a button → <Menu />.
  • The content is a transient acknowledgement → <Toast />.
  • You want a persistent side panel that doesn't darken the page → that's a <SideNav /> pattern (separate component, not yet shipped).

Anatomy

text
<Drawer>
  <Drawer.Trigger> button </Drawer.Trigger>
  <Drawer.Content side="right">
    <Drawer.Close />               (optional × button in the top-end corner)
    <Drawer.Header
      title="…"                   (wired into aria-labelledby)
      description="…"             (wired into aria-describedby)
      avatar={…}
      action={…}
    />
    <Drawer.Body>… scrollable region …</Drawer.Body>
    <Drawer.Footer align="end">
      <Drawer.Close asChild><Button variant="ghost">Cancel</Button></Drawer.Close>
      <Button>Save</Button>
    </Drawer.Footer>
  </Drawer.Content>
</Drawer>
<Drawer>
  <Drawer.Trigger> button </Drawer.Trigger>
  <Drawer.Content side="right">
    <Drawer.Close />               (optional × button in the top-end corner)
    <Drawer.Header
      title="…"                   (wired into aria-labelledby)
      description="…"             (wired into aria-describedby)
      avatar={…}
      action={…}
    />
    <Drawer.Body>… scrollable region …</Drawer.Body>
    <Drawer.Footer align="end">
      <Drawer.Close asChild><Button variant="ghost">Cancel</Button></Drawer.Close>
      <Button>Save</Button>
    </Drawer.Footer>
  </Drawer.Content>
</Drawer>
  • Drawer — context provider. Owns open, the trigger / content refs, the lifecycle hooks (useEscapeStack, useScrollLock), and the ARIA ids.
  • Drawer.Trigger — clones a single child (asChild, default), attaches click + ARIA. Or renders an inline <button> when asChild={false}. Optional — controlled Drawers can omit it.
  • Drawer.Content — the portal-rendered, focus-trapped, edge-anchored, sliding panel. Carries the visual axes (side / size / overlay).
  • Drawer.Header — title + description + optional avatar + action slots. Auto-wires ARIA via the parent context.
  • Drawer.Body — scrollable middle region (flex-1 min-h-0 overflow-y-auto).
  • Drawer.Footer — button-row with align variant (start / center / end / between).
  • Drawer.Close — built-in × button (asChild-able) that calls setOpen(false).

Sides & sizes

The size axis is direction-aware:

SideWhat size controlsDefaultExample tokens
leftDrawer widthmdsm → max-w-xs … xl → max-w-xl … full → 100%
rightDrawer width—(same as left)
topDrawer height—sm → max-h-[20rem] … xl → max-h-[36rem] … full → 100%
bottomDrawer height—(same as top)

The rems for vertical sides are picked to mirror the rem-equivalents of the horizontal max-w-* tokens, so a lg left-drawer and a lg top-drawer occupy the same visual budget along their respective axes.

side and size both accept ResponsiveValue<>, which is the headline pattern Drawer enables:

tsx
<Drawer.Content
  side={{ base: 'bottom', md: 'right' }}
  size={{ base: 'lg', md: 'md' }}
>
  …
</Drawer.Content>
<Drawer.Content
  side={{ base: 'bottom', md: 'right' }}
  size={{ base: 'lg', md: 'md' }}
>
  …
</Drawer.Content>

A bottom-sheet on mobile flips to a right-drawer on desktop without any conditional rendering.

Behavior

PropDefaultEffect
closeOnEscapetrueEsc closes the topmost Drawer (escape-stack ordering).
closeOnBackdropClicktrueClick on the backdrop (not inside Content) closes the Drawer.
trapFocustrueFocus moves into Content on open; Tab cycles inside; focus returns to trigger on close.
preventScrolltrueLocks document.body scroll while open via the engine's reference-counted useScrollLock.
initialFocus(first focusable)Element to focus on open. Defaults to the first focusable inside Content; falls back to Content itself (tabIndex={-1}).
finalFocus(the trigger)Element to focus on close. Defaults to whatever opened the Drawer.

Accessibility

  • Trigger carries aria-haspopup="dialog", aria-expanded, data-state="open"|"closed".
  • Content carries role="dialog", aria-modal="true", aria-labelledby={titleId}, aria-describedby={descId} — both ids resolve to nodes rendered by <Drawer.Header>'s title and description props automatically.
  • Focus management (when trapFocus={true}):
    • On open: focus moves to initialFocus if provided, else first focusable child, else Content itself (which carries tabIndex={-1}).
    • Tab cycles inside Content via the engine's useFocusTrap.
    • On close: focus returns to the trigger (or finalFocus if provided).
  • Escape closes via the engine's useEscapeStack — only the topmost Drawer closes per press.
  • Body scroll is locked via useScrollLock while open, restored on close. Reference-counted with Modal's lock (Drawer-over-Modal collapses to a single lock + unlock pair).
  • Backdrop click uses an e.target === e.currentTarget sentinel — clicks inside Content (inputs, buttons) bubble up but never satisfy the sentinel, so they never close the Drawer.
  • Close button carries aria-label="Close" by default.
  • Side semantics: side is physical (left / right / top / bottom) — RTL does not flip it, because the spatial intuition is what consumers reason about. The inner border uses logical axes (border-s / border-e) so the visual border still lands on the correct edge in RTL.
  • axe-core: zero violations across the side × size × open-state matrix.

Examples

Basic

Loading preview…
Basic.tsx

Sides

Loading preview…
Sides.tsx

Sizes

Loading preview…
Sizes.tsx

Overlays

Loading preview…
Overlays.tsx

NavigationDrawer

Loading preview…
NavigationDrawer.tsx

BottomSheet

Loading preview…
BottomSheet.tsx

ScrollableBody

Loading preview…
ScrollableBody.tsx

FormInside

Loading preview…
FormInside.tsx

NestedDrawer

Loading preview…
NestedDrawer.tsx

ResponsiveSide

Loading preview…
ResponsiveSide.tsx

Controlled

Loading preview…
Controlled.tsx

Programmatic

Loading preview…
Programmatic.tsx

Theming

tsx
defineTheme({
  components: {
    Drawer: {
      defaultProps: { /* root behavior props — closeOnEscape / preventScroll / etc. */ },
      styleOverrides: {
        backdrop: 'bg-fg-default/50',
        content: 'shadow-2xl',
        header: '',
        body: '',
        footer: '',
        close: 'text-fg-muted',
      },
    },
  },
});
defineTheme({
  components: {
    Drawer: {
      defaultProps: { /* root behavior props — closeOnEscape / preventScroll / etc. */ },
      styleOverrides: {
        backdrop: 'bg-fg-default/50',
        content: 'shadow-2xl',
        header: '',
        body: '',
        footer: '',
        close: 'text-fg-muted',
      },
    },
  },
});

Per-instance overrides via <Drawer.Content className sx style /> (and the equivalent on every subpart) merge on top of the theme overrides, which merge on top of the recipe.

Props

PropTypeDefaultDescription
closeOnBackdropClickboolean—Default: `true`. Click on the backdrop closes the Drawer. Set to `false` for destructive- action drawers where an accidental click should not lose work.
closeOnEscapeboolean—Default: `true`. Esc closes the topmost Drawer (escape-stack ordering).
defaultOpenboolean—Initial `open` for the uncontrolled case. Default: `false`.
finalFocusRefObject<HTMLElement | null>—Element to focus when Content unmounts. Defaults to the trigger that opened the Drawer (if there was one); falls back to whatever was focused at open-time.
initialFocusRefObject<HTMLElement | null>—Element to focus when Content mounts. Defaults to the first focusable descendant; if there are none, Content itself (carries `tabIndex={-1}`) is focused.
onOpenChange(open: boolean) => void—Notified whenever `open` changes (controlled or uncontrolled).
openboolean—Controlled `open`. When omitted, Drawer manages its own state via `defaultOpen`.
preventScrollboolean—Default: `true`. Locks `document.body` scroll while open via the engine's reference-counted `useScrollLock` (so a Drawer-over-Modal combo collapses into one lock + unlock pair).
trapFocusboolean—Default: `true`. Focus is trapped inside Content while open and restored on close.