Components
Drawer
A panel that slides in from any edge of the screen, built on Vaul with smooth drag-to-dismiss gestures and a dimmed overlay.
Basic usage
Compose a DrawerTrigger with DrawerContent. State is managed internally — no useState required. Unlike Sheet there is no corner ✕, so always leave a DrawerClose in the footer.
Direction
Set direction on the root to slide in from any edge. Vaul stamps the choice onto the panel as data-vaul-drawer-direction, and the layout follows from there: side drawers cap at sm:max-w-sm, top and bottom at max-h-[80vh] with the rounded edge facing the screen.
Grab handle
Bottom drawers get a grab handle at the top of the panel; the other three directions do not render one. It is painted with bg-control-edge rather than bg-muted on purpose: the bar is the only sign that says “drag here”, so it owes the 3:1 of WCAG 1.4.11 rather than the looser bar for a surface. Against the panel background bg-muted measures 1.07:1 in light and 1.17:1 in dark — effectively invisible — while bg-control-edge lands at 3.83:1 and 4.94:1.
Scrollable content
The header and footer stay pinned while a flex-grown body scrolls. Wrap long content in an overflow-y-auto region.
Drawer or Sheet?
Drawer is built on vaul: the panel follows the finger, carries momentum, and can be flicked away — the right feel on a touch screen. Sheet is a Radix Dialog that slides on a fixed track with a corner close button, which suits desktop filters and detail panels. Reach for Drawer when the gesture matters, Sheet when it does not.
Props
Props below are set on the root Drawer. All subcomponents forward their native and Vaul props.
| Prop | Type | Default | Description |
|---|---|---|---|
direction | 'top' | 'bottom' | 'left' | 'right' | 'bottom' | Edge the drawer slides in from. Set on the root Drawer — vaul writes it onto the content as data-vaul-drawer-direction, which is what the styling keys off. |
open | boolean | — | Controls the open state. Omit for an uncontrolled drawer managed internally. |
defaultOpen | boolean | false | Open on mount, skipping the enter animation. Still reacts to later open changes. |
onOpenChange | (open: boolean) => void | — | Called when the drawer is requested to open or close. |
modal | boolean | true | When true, interaction with outside elements is blocked while open. |
dismissible | boolean | true | When false, dragging, clicking outside and Escape all stop closing the drawer — pair it with a controlled open prop or there is no way out. |
closeThreshold | number | 0.25 | Fraction of the drawer that has to be dragged away before releasing closes it. |
shouldScaleBackground | boolean | false | Scales the page behind the drawer for the iOS-style stacked effect. Requires a [vaul-drawer-wrapper] element around the page content. |
snapPoints | (number | string)[] | — | Optional snap positions for partially-open states, from least to most visible — e.g. [0.4, 0.8, 1] or px strings. |
fadeFromIndex | number | last snap point | Index of the snap point from which the overlay starts to fade. Only valid alongside snapPoints. |
activeSnapPoint | number | string | null | — | Controlled active snap point; pair with setActiveSnapPoint. |
setActiveSnapPoint | (snapPoint: number | string | null) => void | — | Setter for the controlled activeSnapPoint — vaul calls it when the user drags between snap points. |
handleOnly | boolean | false | Restricts dragging to a vaul <Drawer.Handle />. The package draws its own handle as a plain div and does not re-export Handle, so turning this on leaves nothing draggable — leave it off. |
autoFocus | boolean | false | Focus the drawer content when it opens. |
nested | boolean | false | Marks a drawer opened from inside another drawer so the two stack correctly. |
childrenrequired | React.ReactNode | — | Trigger and content subcomponents. |
Parts
Every part accepts className, which is merged onto the defaults with cn().
| Prop | Type | Default | Description |
|---|---|---|---|
Drawer | vaul Drawer.Root | — | Stateful container. Every prop in the table above is set here. |
DrawerTrigger | vaul Drawer.Trigger | — | Element that opens the drawer. Use asChild to render your own Button. |
DrawerPortal | vaul Drawer.Portal | — | Portal target. DrawerContent renders one already — reach for it only when building a custom panel shell. |
DrawerOverlay | vaul Drawer.Overlay | — | Dimmed backdrop (bg-ink/60, dark:bg-ink/80, backdrop-blur). Also rendered for you by DrawerContent. |
DrawerContent | vaul Drawer.Content | — | The panel: portal + overlay + a flex column, plus the grab handle on bottom drawers. It has no built-in close button — supply a DrawerClose yourself. |
DrawerHeader | div | — | Title region, p-4. Text is centred for top and bottom drawers and left-aligned from md up. |
DrawerTitle | vaul Drawer.Title | — | Accessible name for the panel. Required for screen-reader announcements. |
DrawerDescription | vaul Drawer.Description | — | Supporting text linked to the panel via aria-describedby. |
DrawerFooter | div | — | Actions region: mt-auto pins it to the base of the panel, p-4 matches the header. |
DrawerClose | vaul Drawer.Close | — | Dismisses the drawer. Use asChild to wrap a custom Button. |
Accessibility
- Focus is trapped inside the drawer while open and restored to the trigger on close.
- The overlay and surrounding page are marked inert in modal mode, so screen readers stay scoped to the drawer.
- Always include a
DrawerTitle— it labels the dialog for assistive tech; pair it withDrawerDescriptionfor context. - Pressing Esc or clicking the overlay dismisses the drawer unless
dismissibleis false. - The grab handle is decorative and not focusable — dragging is a pointer gesture only, so a
DrawerClosebutton is the keyboard route out and is never optional.