Drawer
A panel that slides in from any edge of the screen. Built on Radix Dialog for focus trap, scroll lock and a11y; tuned for mobile with full-bleed side panels, a bottom-sheet grab handle, and swipe-to-dismiss.
Installation#
Usage#
import {
Drawer, DrawerTrigger, DrawerContent, DrawerHeader,
DrawerBody, DrawerFooter, DrawerTitle, DrawerDescription, DrawerClose,
} from "@/components/ui/drawer"
import { Button } from "@/components/ui/button"
export function Example() {
return (
<Drawer>
<DrawerTrigger asChild>
<Button>Open drawer</Button>
</DrawerTrigger>
<DrawerContent side="right">
<DrawerHeader>
<DrawerTitle>Notifications</DrawerTitle>
<DrawerDescription>You're all caught up.</DrawerDescription>
</DrawerHeader>
<DrawerBody>{/* … */}</DrawerBody>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="ghost">Close</Button>
</DrawerClose>
<Button>Mark all read</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
)
}Anatomy#
import {
Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerTitle, DrawerDescription,
DrawerBody, DrawerFooter, DrawerClose, DrawerNav, DrawerView, DrawerNavBack, DrawerNavTrigger,
} from "@/components/ui/drawer"
<Drawer>
<DrawerTrigger />
<DrawerContent>
<DrawerHeader>
<DrawerTitle />
<DrawerDescription />
</DrawerHeader>
<DrawerBody />
<DrawerFooter>
<DrawerClose />
</DrawerFooter>
</DrawerContent>
</Drawer>
{/* With in-sheet navigation */}
<DrawerContent>
<DrawerNav>
<DrawerView>
<DrawerHeader>
<DrawerNavBack />
<DrawerTitle />
</DrawerHeader>
<DrawerBody>
<DrawerNavTrigger />
</DrawerBody>
</DrawerView>
</DrawerNav>
</DrawerContent>Sides#
side on DrawerContent picks the edge the panel slides from: top, right (default), bottom, or left. Top and bottom render as sheets with a grab handle; left and right as full-height panels. Every variant animates with the dedicated ease-drawer curve.
Mobile: bottom sheet#
On touch devices the bottom sheet is the native pattern. It rounds its top corners, shows a grab handle, and pads past the home-indicator safe area. It is swipe-to-dismiss by default. Drag the handle or header down and release past ~45% (or flick) to close, handing the exit animation back to Radix. The drag only arms after a few pixels, so taps on the rows still register, and it never starts on the scrollable body, so a drag and a scroll never fight. Disable it with swipeToClose={false}.
Navigate inside the sheet#
A bottom sheet often needs to drill into nested views (an action sheet that opens a sub-panel, a settings menu, a multi-step filter) without ever leaving the sheet. Wrap the pages in DrawerNav and give each a DrawerView: only the active view renders, it enters with a native horizontal push (from the right going forward, the left on back), and the sheet's height animates to fit it. A DrawerNavTrigger drills in; a DrawerNavBack (which renders nothing at the root, so you can place it in a shared header) pops. The stack resets when the sheet closes, and swipe-down still dismisses the whole sheet from any view.
Form panel#
A right-side panel is the desktop home for an edit form; it keeps the underlying page visible for context. Compose labelled Input parts in the DrawerBody and land focus on the first field with onOpenAutoFocus.
Navigation menu#
A left drawer makes a compact, off-canvas navigation menu for small screens. The DrawerBody scrolls independently while the header and footer stay pinned; each link is wrapped in DrawerClose so picking one dismisses the panel.
Sizes#
size caps the panel's cross-axis extent: the width of left/right panels and the height of top/bottom sheets. Panels stay full-bleed below the cap, so on a phone they fill the screen and on desktop they settle into a panel. Four steps: sm, md (default), lg, xl.
Density#
Drawers are compact unless a DensityProvider or density="comfortable" opens them up: 24px padding and an 18px title instead of 16px and 16px. The content re-provides its density to the header, body and footer so every part stays in sync. Best driven once via DensityProvider. See Density.
API reference#
Drawer
The root, Radix Dialog.Root; renders no DOM. Every part that renders an element accepts className, merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | - | Controlled open state. |
| defaultOpen | boolean | false | Initial open state when uncontrolled. |
| onOpenChange | (open: boolean) => void | - | Called when the drawer opens or closes, including by a swipe. |
| modal | boolean | true | Trap focus, lock scroll and hide the rest of the page from screen readers while open. |
DrawerTrigger
Opens the drawer, Radix Dialog.Trigger. Renders a <button> and forwards all its props.
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | false | Merge onto your own element (a Button) instead of rendering a <button>. |
| Data attribute | Description |
|---|---|
| data-state | "open" or "closed". |
DrawerContent
The panel, Radix Dialog.Content portalled to the body with its own scrim; forwards all its props. Re-provides its density to every part inside.
| Prop | Type | Default | Description |
|---|---|---|---|
| side | "top" | "right" | "bottom" | "left" | "right" | The edge it slides from. Top and bottom are sheets with a grab handle; left and right are full-height panels. |
| size | "sm" | "md" | "lg" | "xl" | "md" | Caps the width of a side panel (max-w-sm to max-w-xl) or the height of a sheet (40 to 92dvh). |
| density | "comfortable" | "compact" | "compact" | 24px padding with an 18px title, or 16px with a 16px title. Falls back to the nearest DensityProvider. |
| swipeToClose | boolean | true | Drag the handle or header toward the edge to dismiss: past about 45% of the panel, or on a flick. |
| showHandle | boolean | true | Show the grab handle on top and bottom sheets. |
| showClose | boolean | true | Render the built-in top-right close button. |
| closeLabel | string | "Close" | Accessible label of the built-in close button. |
| onOpenAutoFocus | (event: Event) => void | - | Called as focus moves in on open; preventDefault() and focus your first field instead. |
| onCloseAutoFocus | (event: Event) => void | - | Called as focus returns to the trigger on close. |
| onEscapeKeyDown | (event: KeyboardEvent) => void | - | Called on Escape; preventDefault() keeps the drawer open. |
| onInteractOutside | (event: Event) => void | - | Called on a pointer down or focus outside the panel; preventDefault() keeps it open. |
| Data attribute | Description |
|---|---|
| data-state | "open" or "closed", on the panel and on the scrim. |
| data-side | The resolved side. |
| CSS variable | Description |
|---|---|
| --surface | Set to var(--popover), read by nested Inputs and Selects. |
DrawerHeader
The title block at the panel's edge, and a drag region for swipe-to-dismiss. Forwards all native <div> props.
DrawerTitle
The heading the drawer is announced by, Radix Dialog.Title (an <h2>). Required for accessibility; hide it with sr-only when the design has none.
DrawerDescription
Muted supporting copy, Radix Dialog.Description (a <p>).
DrawerBody
The region between header and footer that scrolls on its own; a swipe never starts here. Forwards all native <div> props.
DrawerFooter
The action row pinned to the panel's far edge: stacked and reversed on mobile, right-aligned from sm. Forwards all native <div> props.
DrawerClose
Closes the drawer, Radix Dialog.Close. Renders a <button> and forwards all its props.
| Prop | Type | Default | Description |
|---|---|---|---|
| asChild | boolean | false | Merge onto your own element (a footer Button, a nav link) instead of rendering a <button>. |
DrawerNav
An in-sheet navigation stack holding DrawerView pages; its height animates to the active one and it resets each time the drawer opens. Forwards all native <div> props except onChange.
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultView* | string | - | The view shown when the drawer opens. |
| onViewChange | (view: string) => void | - | Called whenever the active view changes, on a push or a pop. |
DrawerView
One page in the stack, rendered only while active; it pushes in from the right going forward and from the left on back. Forwards all native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| view* | string | - | The id this page is shown for. |
| Data attribute | Description |
|---|---|
| data-direction | "forward" or "back" after the first navigation; absent on the first view. |
DrawerNavTrigger
Pushes a view onto the stack when clicked, unless your onClick calls preventDefault(). Forwards all native <button> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| view* | string | - | The DrawerView to open. |
| asChild | boolean | false | Merge onto your own row or button instead of rendering a <button>. |
DrawerNavBack
An icon button that pops the stack; renders nothing at the root view, so it can sit in a shared header. Forwards all native <button> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | "Back" | Accessible label. |
| children | React.ReactNode | - | Replaces the default caret glyph. |
useDrawerNav
A hook for driving the stack from anywhere inside a DrawerNav, e.g. after an async action. Returns { activeView, canGoBack, navigate(view), back() }.