Skip to main content

Drawer

Implemented

Side panel dialog (portal, scrim, Escape, scroll-lock). Always renders a header row — title <h2> left, close IconButton right — above a scrolling body (flex column, flex-1 overflow-y-auto). Placements left/right, sizes. Real implementation; the organisms/drawer stories twin was removed per ADR-035.

Categorymolecules
Sourcecomponents/molecules/Drawer/Drawer.tsx
AI HintOverlay surface for contextual panels (cart, filters, admin forms). The header row is built in: pass title + closeLabel, never render your own header or close button inside children. The body scrolls, so no max-h/overflow compensation in children.

Side-anchored dialog molecule: portal, scrim, Escape, scroll lock. Placements left/right; sizes sm 288 / md 384 / lg 448 / full.

Drawer

Feedback & Overlay — Family 4

Controlled overlay: pass open + onClose; always set title or ariaLabel so the dialog is named. Traps focus (Tab/Shift+Tab cycle) and returns focus to the trigger on close. Appears instantly — no animation (tracked).

This component supports light and dark mode.

Usage

Drawer is a controlled component — pair open with a piece of local state and wire onClose so the scrim click and the Escape key dismiss it. The panel portals to document.body, locks body scroll while open, and restores it on close. Always pass title or ariaLabel so the dialog is named (WCAG 4.1.2).

import { Drawer, Button } from '@epasslive/design-system'

const [open, setOpen] = useState(false)

<Button variant="primary" onClick={() => setOpen(true)}>Open cart</Button>
<Drawer
  open={open}
  onClose={() => setOpen(false)}
  placement="right"
  size="lg"
  title="Your cart"
>
  <p>Cart contents go here.</p>
</Drawer>

Drawer vs Modal vs CartDrawer

  • Blocking, centered task (confirm, short form) → Modal (max-w sizes fit small viewports).
  • Side-anchored navigation or secondary content (filters, cart, details) → Drawer (this molecule).
  • A complete cart UI (lines, totals, checkout CTA, fr/en labels) → CartDrawer — an organism that composes this Drawer; bring your own chrome for anything else.
  • App-level navigation on mobile → Sidebar(nav-specific chrome: localized close label and an 85vw mobile width guard).
  • Drawer now traps focus and returns it to the trigger on close (mol51 step 6); Modal does not yet — if keyboard containment is required in a centered overlay, wire it via composition until Modal adopts the same fixes.

Accessibility

  • Renders role="dialog" with aria-modal="true", portalled to document.body; named via title (h2 wired through aria-labelledby) or ariaLabel (takes precedence). If neither is passed the dialog is unnamed — always set one (WCAG 4.1.2).
  • Escape closes (document keydown, stopPropagation); scrim is a real <button> whose accessible name comes from closeLabel — default "Close drawer" (English); pass a localized string for non-English apps.
  • Body scroll is locked while open and restored on close. On open the panel container receives initial focus; while open, a focus trap cycles Tab/Shift+Tab within the panel (WCAG 2.4.3); on close, focus returns to the trigger (the element focused when the drawer opened).
  • Known gap (fix tracked): no open/close animation — the panel appears and disappears instantly. Top/bottom placement is not implemented (left/right only); panels carry a max-w-full guard so sm/md/lg never exceed the viewport width.
  • Color contrast meets WCAG AA in both themes (token-driven surface/border/heading colors).

Props

Prop
Type
Default
Description
open*
boolean
—

Drawer open state. Renders nothing (empty) when false.

onClose*
() => void
—

Called on scrim click and on Escape.

placement
'left' | 'right'
right

Side the panel anchors to. Physical values only (no logical start/end; no RTL mirroring; top/bottom not implemented).

size
'sm' | 'md' | 'lg' | 'full'
md

Panel width: sm = 288px, md = 384px, lg = 448px, full = 100vw. Panels carry a max-w-full guard — sm/md/lg never exceed the viewport width.

title
string
—

Renders an h2 inside the panel; its id keys aria-labelledby.

ariaLabel
string
—

Accessible name override; takes precedence over title. If neither is set the dialog has NO accessible name (known gap).

closeLabel
string
"Close drawer"

Accessible name for the scrim close button. Pass a localized string (e.g. "Fermer") so screen readers announce it in the app language.

children*
React.ReactNode
—

Panel content.

id
string
—

Stable id prefix for the title element (aria-labelledby wiring); defaults to `drawer-<useId>`.

Props

Preview

Drawer

Prop
Type
Required
Default
Description
open
boolean
Required
—

Whether the element is open/expanded.

onClose
() => void
Required
—

Called when the drawer should close (Escape, scrim click, or the header close button).

title
string
Optional
—

Header text. Rendered as an <h2> on the left of the header row and wired to the dialog's aria-labelledby. Omitted, the header row shows only the close button.

closeLabel
string
Optional
'Close drawer'

Accessible name shared by the header close IconButton and the scrim button — pass a translated string.

placement
DrawerPlacement
Optional
'right'

Which edge the panel slides in from — 'left' | 'right'.

size
DrawerSize
Optional
'md'

Panel width preset — 'sm' | 'md' | 'lg' | 'full'.

ariaLabel
string
Optional
—

Overrides the dialog's accessible name when no title is given.

children
React.ReactNode
Required
—

Body content, rendered inside the scrolling region under the header. Keep scroll/overflow styling out of children — the body already scrolls.

id
string
Optional
auto

Overrides the generated drawer id prefix.

Quick use:<Drawer />

Accessibility

ARIA roledialog

a11y

1 rule

WCAG 2.1.1 / 4.1.2

the header close button is always rendered and carries closeLabel as its aria-label (UAT-19)

Keyboard

1 rule

WCAG 2.1.2

Escape closes; focus returns to the opener (useOverlayDialog)

Usage

Import

tsxSnippet
import { Drawer } from "@epasslive/design-system"

Example 1

tsxSnippet
<Drawer open={open} onClose={close} title="Your cart" closeLabel="Close cart">…</Drawer>

Anti-patterns

— Do not render a second header or close control inside children

— Do not add max-h/overflow wrappers around children — the body already scrolls

— Avoid inline styles on atoms

Design System v4.1.0 | ePassLive