Drawer
Headless modal side-sheet primitives with drawer-specific parts and placement metadata.
Drawer in motion
When to Use
Use Drawer when a modal task or navigation panel should enter from an edge of the screen. Use Dialog when the panel belongs in the center or has no edge meaning. Use an ordinary inline panel when the page should remain fully usable while it is open. Drawer provides behavior and placement metadata, but your application owns its visual position and motion.
Features
The behavior Atom owns before your product adds appearance.
- Supports controlled and uncontrolled open state with close reasons.
- Traps and restores focus, locks scrolling, and contains descendant portals.
- Supports independent Escape, backdrop, and Overlay dismissal controls.
- Exposes consumer-provided placement through
[data-placement]. - Supports keep-mounted content for consumer-owned transitions.
- Provides generated title and description relationships.
- Remains headless: placement does not apply layout or animation.
Import
Anatomy
API Reference
Root
Owns drawer state, dismissal settings, generated IDs, and focus restoration. It renders no wrapper.
| Prop | Type | Default |
|---|---|---|
open | boolean | - |
defaultOpen | boolean | false |
onOpenChange | (open: boolean, reason?: ModalCloseReason) => void | - |
closeOnEscape | boolean | true |
closeOnBackdropClick | boolean | true |
disabled | boolean | false |
keepMounted | boolean | false |
Trigger
Opens the drawer. It renders a native button by default and supplies button
semantics when composed onto a custom element.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
render | RenderProp | - |
| ARIA attribute | Values |
|---|---|
role | "button" for custom elements |
aria-haspopup | "dialog" |
aria-expanded | Current open state |
aria-controls | Content ID while open |
aria-disabled | "true" when Root is disabled |
| Data attribute | Values |
|---|---|
[data-slot] | "drawer-trigger" |
[data-state] | "open" | "closed" |
[data-disabled] | Present when disabled |
Portal
Moves Overlay and Content to document.body by default without creating a
wrapper.
| Prop | Type | Default |
|---|---|---|
container | HTMLElement | null | document.body |
disabled | boolean | false |
The container must be an HTMLElement in the current document. ShadowRoot,
DocumentFragment, and cross-document containers are unsupported. Atom keeps
the ancestor paths to separate Overlay and Content portals, inline Content,
nested same-document containers, and registered branches active while making
background subtrees inert. Global Toast/live-region containers are background
unless they are within an owned path or registered with Modal.Branch.
Overlay
Renders the accessibility-hidden backdrop. Clicking it closes with reason
"backdropClick" unless Root or this Overlay disables backdrop dismissal.
Overlay and Content must be siblings; Atom rejects Content nested beneath the
Overlay's aria-hidden subtree. Bubbled clicks from Overlay descendants are not
backdrop clicks and do not dismiss the Drawer.
Separate portals are valid when the committed Content DOM is outside Overlay.
| Prop | Type | Default |
|---|---|---|
disabled | boolean | false |
| ARIA attribute | Values |
|---|---|
aria-hidden | "true" |
| Data attribute | Values |
|---|---|
[data-slot] | "drawer-overlay" |
[data-state] | "open" | "closed" |
[data-positioned] | Present after the opening frames |
Content
Renders the focus-trapped panel as a div. placement is copied to a data
attribute only; it does not apply positioning.
| Prop | Type | Default |
|---|---|---|
placement | string | - |
aria-label | string | - |
aria-labelledby | string | generated from Title when present |
aria-describedby | string | undefined | generated from Description when present |
ariaLabel | string | compatibility fallback |
initialFocus | ModalFocusTarget<ModalInitialFocusDetails> | interaction-aware default |
finalFocus | ModalFocusTarget<ModalFinalFocusDetails> | prior focus, then Trigger |
| ARIA attribute | Values |
|---|---|
role | "dialog" |
aria-modal | "true" while open |
aria-hidden | "true" while retained only for exit presence |
inert | Present while retained only for exit presence |
aria-label | Explicit native value, otherwise ariaLabel compatibility value |
aria-labelledby | Explicit native value, otherwise registered Title ID |
aria-describedby | Explicit native value, otherwise registered Description ID |
| Data attribute | Values |
|---|---|
[data-slot] | "drawer-content" |
[data-state] | "open" | "closed" |
[data-placement] | Consumer-provided placement string |
[data-positioned] | Present after the opening frames |
With keepMounted, closed Content remains inside a hidden, aria-hidden
wrapper and preserves its class name and placement metadata.
Exit-animated Content retained after close immediately loses aria-modal and
becomes inert and accessibility-hidden. Background isolation, focus ownership,
and active scroll containment end when open becomes false, not when the visual
exit ends.
Title
Provides the heading referenced by Content. It renders an h2 by default.
| Prop | Type | Default |
|---|---|---|
as | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "h2" |
| Data attribute | Values |
|---|---|
[data-slot] | "drawer-title" |
Description
Provides supporting text referenced by Content. It renders a native p.
| Data attribute | Values |
|---|---|
[data-slot] | "drawer-description" |
Close
Closes Root with reason "closeClick". It renders a native button by
default and supports custom composition.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
render | RenderProp | - |
| Data attribute | Values |
|---|---|
[data-slot] | "drawer-close" |
The Drawer entry point also exports useModalContext and useModalContent for
advanced custom modal parts. The namespaced parts are preferred for ordinary
drawers because they provide the complete contract above.
For a consumer-owned third-party portal outside Content, use
<Modal.Branch asChild> around its top-level element or configure the portal to
target the Drawer Content element. Nested modal layers suspend the parent
Drawer's trap, dismissal, and scroll ownership while the child is topmost.
Long Drawer Content and registered portalled controls remain scrollable while
background wheel/touch movement and scroll chaining are blocked.
Examples
Navigation Drawer
Controlled Drawer
Accessibility
Drawer uses the
WAI-ARIA Modal Dialog pattern.
Give every drawer a Title or native aria-label, and add Description when the
purpose needs more explanation. ariaLabel remains a compatibility fallback;
native ARIA is preferred and takes precedence. Atom omits aria-describedby
when no Description is registered. Focus moves inside on open, stays within the drawer and
its registered descendant portals, and restores after close. Touch opening
focuses Content by default instead of immediately focusing an input; native
autoFocus and explicit initialFocus take precedence. Use finalFocus for a
different next workflow target or false to suppress automatic restoration.
its registered descendant portals, and returns after close.
| Key | Description |
|---|---|
Escape | Closes the topmost drawer when closeOnEscape is enabled. |
Tab | Moves to the next focusable element in the drawer scope. |
Shift+Tab | Moves to the previous focusable element in the drawer scope. |
Enter / Space | Opens from Trigger and activates native button controls. |
Changelog
Unreleased
- Added public Agent Knowledge for component selection, required composition, recurring mistakes, and validation.
0.20.3
- Inherited document-only overflow locking so sticky application chrome remains anchored while Drawer is open at a nonzero page scroll position.
0.6.7
- Inherited root/body overflow locking without fixed-body repositioning or unlock-time scroll restoration, avoiding iOS Safari browser-toolbar transitions and top-edge clipping caused by the compact address bar.
0.3.4
- Inherited corrected scroll-lock compensation so opening or closing Drawer no
longer shifts pages that use
scrollbar-gutter: stable.
0.3.2
- Inherited corrected nested-modal isolation handoff and final
inertrestoration from the shared Modal foundation.
0.3.1
- Fixed exit-presence cleanup so closed Drawer Content and Overlay unmount after their CSS motion window even when no end event is emitted.
0.3.0
- Fixed native Content ARIA precedence and optional Description relationships.
- Added SSR-safe, hydration-stable Title and Description registration.
- Added Content-level
initialFocusandfinalFocus, including touch-safe Content focus and explicit post-close workflow targets. - Added shared top-layer ownership and
Modal.Branchsupport for third-party drawer portals. - Preserved descendant composite and nested-modal keyboard contracts through metadata-aware focus containment.
- Added long-content and registered-portal scroll allowances with background wheel/touch containment and exact cleanup.
- Added stack-aware background isolation and same-document
HTMLElementportal-container enforcement through the shared Modal foundation. - Established shared modal ownership before paint; exit-present Content is inert and accessibility-hidden, nested lock handoff is uninterrupted, and unavailable Tab candidates are skipped.
- Rejected Content nested beneath Overlay and prevented bubbled descendant clicks from being treated as backdrop dismissal.
0.2.0
- Added shared dismissable layer Escape handling so nested overlays close before the parent drawer closes.
- Improved modal focus containment, including support for registered portalled layers owned by descendants inside the drawer.
- Preserved
classNameon keep-mounted hidden drawer content.
0.1.0
- Initial Atom release.