Drawer
A bottom sheet that slides up and can be swiped to dismiss — mobile-first.
@garn/ui/drawerOn this page
Default
Trigger → bottom drawer with footer actions.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add drawer- Registry
drawer- Deps
vaul- Registry deps
utils
Import
import { Drawer, DrawerTrigger, DrawerContent, DrawerClose, DrawerTitle, DrawerDescription, DrawerHeader, DrawerFooter } from "@garn/ui/drawer";Anatomy
The parts this component is built from — compose them to assemble it.
DrawerRequiredRoot — owns the open state; shouldScaleBackground (default true) scales the page behind it.
DrawerTrigger[data-slot="drawer-trigger"]Opens the drawer (asChild to wrap your own button).
DrawerContentRequired[data-slot="drawer-content"]The bottom panel; portals its overlay and renders a centered drag handle.
DrawerClose[data-slot="drawer-close"]Closes the drawer (e.g. a Cancel button).
DrawerTitleRequired[data-slot="drawer-title"]Accessible name for the drawer (required; sr-only if visually hidden).
DrawerDescription[data-slot="drawer-description"]Supplementary description, wired to aria-describedby.
DrawerHeader[data-slot="drawer-header"]Layout wrapper for title + description (panel padding).
DrawerFooter[data-slot="drawer-footer"]Bottom-pinned wrapper for actions (panel padding).
Props
The component's public props and their types.
No component-specific props — this is a thin wrapper over its native element.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="drawer-content"]States
| Selector | State | Description |
|---|---|---|
[data-state=open] | open | Content + overlay mounted and slid up. |
[data-state=closed] | closed | Sliding/swiping down before unmount. |
| Property | Token | Tier |
|---|---|---|
padding | --garn-pad-panel | semantic · air |
headerGap | --garn-gap-field | semantic · air |
When to use
Surface a secondary task or details from the bottom edge on touch/mobile — quick settings, a picker, contextual actions you can swipe away.
Reach for something else when
- A desktop side panel (use sheet)
- a small focused confirmation (use dialog / alert-dialog)
- contextual content anchored to a control (use popover).
Overview
Drawer surfaces a secondary task from the bottom edge on touch/mobile — quick settings, a picker, contextual actions you can swipe away. DrawerContent renders a centered drag handle and the page scales back behind it (shouldScaleBackground), giving the native bottom-sheet feel; DrawerTitle (required) names it. It's the mobile counterpart to a Sheet: on the desktop, an edge Sheet or a Popover usually fits better than a bottom drawer.
Guidelines
Use the drawer where the bottom-sheet gesture belongs — touch. It's tuned for thumbs: swipe-to-dismiss and a drag handle. On pointer-first layouts, prefer a right Sheet or an anchored Popover instead of forcing a bottom sheet.
Keep it short enough to swipe away. A drawer is for a quick, dismissible task; if it needs to fill the screen or scroll heavily, that's really a full page or a Sheet. Pin primary actions in DrawerFooter within thumb reach.
Always render a DrawerTitle and respect dismissal. It's the accessible name (sr-only if visually hidden); swipe-down, the overlay, and Escape all close. Don't trap a destructive, no-undo action behind a gesture that's easy to trigger by accident.
Best practices
- Always include a DrawerTitle (sr-only if you don't want it visible).
- Keep content short — a drawer is a quick, dismissible surface, not a full page.
- Provide an explicit action/close in the footer in addition to swipe-to-dismiss.
- Don't use a drawer for primary desktop layouts — prefer sheet there.
- Don't omit the DrawerTitle.
- Don't pack a long form into a bottom sheet — promote it to a page or dialog.
Content guidelines
- Give the DrawerTitle a short, specific name for the panel.
- Keep the content focused on one task and label actions by outcome.
Troubleshooting
The page behind the drawer doesn't scale (or looks odd) with shouldScaleBackground.
Cause. The scale effect needs the app root marked data-vaul-drawer-wrapper with a background set.
Fix. Wrap the app root in [data-vaul-drawer-wrapper] (or pass shouldScaleBackground={false}).
Screen readers can't name the drawer / a console warning about a missing title.
Cause. DrawerContent without a DrawerTitle.
Fix. Add a DrawerTitle (sr-only if hidden) and a DrawerDescription.
Accessibility
- Role
dialog- ARIA APG
- dialog (modal)
- Focus
- Focus trapped inside the content; returns to the trigger on close.
Accessibility requirements
Every DrawerContent must contain a DrawerTitle (wrap it in sr-only to hide it visually).
when DrawerContent has no DrawerTitle
Keyboard
| Escape | Close the drawer. |
| TabShift+Tab | Cycle focus within the drawer (focus is trapped). |
- vaul wraps Radix Dialog — aria-modal, labelled by DrawerTitle, described by DrawerDescription.
- Swipe/drag down dismisses it; the same close paths (Escape / overlay / DrawerClose) work for keyboard and pointer.
- Focus moves into the drawer on open and returns to the trigger on close.