Drawer is a thin wrapper around Vaul. It renders a panel that slides up from the bottom of the viewport, sits at partial height (up to 82vh), and dismisses when the user drags it down, swipes past the threshold, clicks the overlay, or presses escape. Reach for it on mobile, or for secondary flows where a full-page modal would feel too heavy. A settings sheet behind a list item, a date-range picker triggered from a toolbar, a confirmation for an action the user initiated by tapping a row.
Pick Dialog instead when the flow is desktop-first, genuinely modal (the user cannot continue without resolving it), or needs to be centered in the viewport for emphasis. Drawer keeps the page visible above it and invites dismissal. Dialog demands attention. If you need a side-anchored panel that stays open while the user works around it, SlidePanel is the right primitive.
Import
import { Drawer } from "@unkey/ui";
Drawer ships as a compound component. Drawer.Root owns the open state,
Drawer.Trigger is the control that opens it, Drawer.Content is the sliding
panel, and Drawer.Title / Drawer.Description label the contents for
assistive technology. Drawer.Nested lets you stack a second drawer on top of
an open one.
Basic
Wrap a Drawer.Trigger asChild around the control that opens the drawer, and
put the body inside Drawer.Content. The overlay, portal, and rounded-top
chrome are applied automatically.
Controlled
Pass open and onOpenChange to Drawer.Root when the parent needs to drive
visibility (for example, to open a drawer from a keyboard shortcut, a URL
param, or another component). Omit the Drawer.Trigger and open the drawer
from whatever handler you like.
Nested
Drawer.Nested stacks a second drawer on top of an open one. The parent
drawer slides back slightly to signal depth, and the nested drawer inherits
the same drag-to-dismiss gestures. Use this sparingly. Two levels of stacking
is usually the limit before the interaction gets confusing.
Accessibility
Vaul implements the WAI-ARIA dialog pattern. Drawer.Content carries
role="dialog" and aria-modal="true", focus is trapped inside while open,
and focus returns to the trigger on close. Escape closes the drawer, clicking
the overlay closes the drawer, and dragging past the dismiss threshold closes
the drawer.
Always render a Drawer.Title inside Drawer.Content. Vaul (like Radix
Dialog) wires the title into aria-labelledby so screen readers announce the
drawer with a name. If the title would be visually redundant, wrap it in
VisuallyHidden rather than omitting it. Pair it with Drawer.Description
whenever the drawer’s purpose needs more than a heading to be understood.
Because drawers are dismissable by drag, keep the primary action reachable by keyboard and do not rely on swipe gestures as the only way to confirm or cancel. Every drawer should have an explicit close affordance inside its content for keyboard and assistive-technology users.
Props
Drawer.Root
The state container. Owns open/close state and coordinates dismissal
behavior. All Vaul Root props pass through.
open boolean Optional Controlled open state. Pair with onOpenChange when the parent drives
visibility.
defaultOpen boolean Optional
Default false Uncontrolled initial open state. Use when the drawer manages itself through its trigger.
onOpenChange (open: boolean) => void Optional Fires on every open/close transition (trigger click, escape, overlay click, drag dismissal, programmatic change).
modal boolean Optional
Default true When true, the overlay blocks interaction with the page behind. Set to
false for a non-modal sheet that lets the user scroll or click through
to the page below.
dismissible boolean Optional
Default true When false, the drawer cannot be closed by drag, overlay click, or
escape. Use for flows the user must complete, and pair with a visible
action to close programmatically.
shouldScaleBackground boolean Optional
Default false When true, scales the page behind the drawer slightly, matching the iOS
sheet aesthetic. Requires a [data-vaul-drawer-wrapper] ancestor on the
page.
snapPoints (string | number)[] Optional Percentages ("50%") or pixel offsets the drawer snaps to. Pair with
activeSnapPoint / setActiveSnapPoint for controlled snapping.
closeThreshold number Optional
Default 0.25 Fraction of the drawer’s height the user must drag past before release dismisses it.
children ReactNode Optional Typically a Drawer.Trigger and a Drawer.Content.
Drawer.Trigger
The control that opens the drawer. Render with asChild so an existing
Button keeps its own focus ring and semantics.
asChild boolean Optional
Default false Merge the trigger’s behavior onto its single child instead of rendering a
wrapping <button>.
children ReactNode Optional The trigger content. With asChild, must be a single focusable element.
Drawer.Content
The sliding panel itself. Rendered into a portal with an overlay underneath, anchored to the bottom of the viewport, capped at 82vh, with a rounded top edge and a drop shadow.
className string Optional Additional Tailwind classes. Merged with the default chrome via cn.
Commonly used to constrain max-h below 82vh or add internal padding.
onOpenAutoFocus (event: Event) => void Optional Fires when focus moves into the drawer on open. Call event.preventDefault()
to keep focus on the trigger.
onCloseAutoFocus (event: Event) => void Optional Fires when focus returns on close. Call event.preventDefault() to route
focus elsewhere.
onEscapeKeyDown (event: KeyboardEvent) => void Optional Fires before escape closes the drawer. Call event.preventDefault() to
override.
onPointerDownOutside (event) => void Optional Fires on pointer-down outside the drawer (overlay or outside the portal). Prevent default to keep the drawer open.
children ReactNode Optional The drawer body. Should include a Drawer.Title (visually or via
VisuallyHidden) for accessible naming.
Drawer.Title
Renders the accessible name for the drawer. Style as a heading at the call site.
className string Optional Additional Tailwind classes. Apply your heading styles here.
children ReactNode Optional A short phrase. Use VisuallyHidden around it when a visible title would
duplicate a label already on screen.
Drawer.Description
Supporting copy wired into aria-describedby. Optional but recommended when
the title alone doesn’t explain the drawer’s purpose.
className string Optional Additional Tailwind classes.
children ReactNode Optional One or two sentences. Longer prose belongs in the body.
Drawer.Nested
A second-level drawer that stacks on top of an open parent. Accepts the same
props as Drawer.Root and is used identically.
children ReactNode Optional A nested Drawer.Trigger and Drawer.Content pair.
Related
- Dialog — for centered, desktop-first modals that demand full focus.
- SlidePanel — for a side-anchored panel that can stay open while the user interacts with the page.