Skip to main content

Drawer

A drawer is a Headless UI Dialog whose panel slides in from an edge of the viewport. It shares the Dialog contract (open / onClose, focus trap, scroll lock, backdrop) and adds side, size and the layout parts. The panel is portalled to <body> and renders nothing on the server.

import Drawer from '@zuilib/components/drawer'

Basic

Drawer renders the backdrop; you place Drawer.Panel inside it and the header, body and footer parts inside that. Drawer.Close with no children is an icon button pinned to the header's top-right corner.

Loading example

Sides

side picks the edge; the slide transition and the panel's border follow it. The parts read side from context, so nothing else changes.

Loading example

Sizes

size is the panel's width for left / right and its height for top / bottom, read from --drawer-size-sm/md/lg (20, 28 and 40rem by default). full covers the viewport.

Loading example

Non-dismissible

dismissible={false} swallows Escape and backdrop clicks. The drawer then closes only through Drawer.Close, which always calls onClose, or by flipping open yourself.

Loading example

Props

DrawerProps extends Headless UI DialogProps (initialFocus, role, autoFocus, unmount, static, …) with:

PropTypeDefaultDescription
openrequiredboolean
onCloserequired() => voidCalled from Drawer.Close, and on Escape / backdrop click while dismissible. Flip open to false in response
side'left' | 'right' | 'top' | 'bottom''right'The edge the panel is pinned to and slides from
size'sm' | 'md' | 'lg' | 'full''md'Width (left / right) or height (top / bottom) from --drawer-size-*; full covers the viewport
dismissiblebooleantruefalse ignores Escape and backdrop clicks
classNamestringMerged last onto the root Dialog element
backdropClassNamestringMerged last onto the backdrop
childrenReactNodeDrawer.Panel and anything else inside the dialog

Parts

Each part is also a named export (DrawerPanel, DrawerTitle, …) and takes className, merged last.

PropTypeDefaultDescription
Drawer.PanelDialogPanel propsThe sheet. Fixed to side, sized by size, slides while closed. Headless UI traps focus inside it
Drawer.TitleDialogTitle propsAn <h2> the dialog is labelled by
Drawer.DescriptionDescription propsA <p> the dialog is described by
Drawer.Headerdiv propsRelative box with end padding reserved for the pinned Drawer.Close
Drawer.Bodydiv propsThe scrolling middle
Drawer.Footerdiv propsActions: stacked (primary on top) on narrow screens, end-aligned row from sm up
Drawer.CloseButton props + label?: string + children?label: 'Close'Calls onClose even when not dismissible. No children: a 32px icon button pinned top-right. With children: a wrapper button with only the focus ring, so wrap a Button

Slots

SlotElementNotes
[data-slot="drawer"]divRoot. data-side, data-size, data-state="open" | "closed", data-dismissible (present while dismissible)
[data-slot="drawer-backdrop"]divFixed scrim, foreground at 50%, fades
[data-slot="drawer-panel"]divThe sheet. data-side, data-size, plus Headless UI data-closed during transitions
[data-slot="drawer-header"]div
[data-slot="drawer-title"]h2
[data-slot="drawer-description"]p
[data-slot="drawer-body"]div
[data-slot="drawer-footer"]div
[data-slot="drawer-close"]button
[data-slot="drawer-close-icon"]svgThe default X, only without children
/* Consumer CSS: wider drawers everywhere, and no border on the sheet */
:root {
--drawer-size-md: 32rem;
}
[data-slot="drawer-panel"] {
border: 0;
}

Tokens

TokenUsed for
--drawer-size-sm/md/lgPanel extent per size
--background, --foregroundPanel surface and text
--borderPanel edge, header and footer dividers
--shadow-elevatedPanel shadow
--foreground at 50%Backdrop
--muted-foreground, --accent, --accent-foregroundIcon close button and its hover
--ringKeyboard focus ring
--duration-slowSlide and fade

Accessibility

  • Renders a Headless UI Dialog: role="dialog", aria-modal="true", focus trapped inside the panel and returned to the opener on close, page scroll locked, the rest of the page inert.
  • Drawer.Title and Drawer.Description wire aria-labelledby and aria-describedby on the dialog; include a title.
  • Escape closes while dismissible; a backdrop click too. With dismissible={false} provide a visible Drawer.Close.
  • The icon Drawer.Close is a 32px target with a visually hidden name (label, default "Close").
  • The slide and fade transitions run through Headless UI's transition; the panel and backdrop are removed from the DOM when closed.
  • Because the panel is portalled, subtree token overrides around the opener do not reach it; retheme on :root or the .dark class.
  • Dialog: the same foundation, centred.
  • Popover: a lightweight anchored panel without a focus trap.
  • Theming: --drawer-size-* and the surface tokens.