Skip to content

Drawer

A bottom sheet that slides up and can be swiped to dismiss — mobile-first.

Stablev0.4.2added in v0.1.0@garn/ui/drawer
On this page

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.

terminal
npx garn-ui add drawer
First time? Set up garn in your project
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.

DrawerRequired

Root — 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

SelectorState
[data-state=open]open
[data-state=closed]closed
PropertyTokenTier
padding--garn-pad-panelsemantic · air
headerGap--garn-gap-fieldsemantic · 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

Do
  • 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
  • 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

warn

Every DrawerContent must contain a DrawerTitle (wrap it in sr-only to hide it visually).

when DrawerContent has no DrawerTitle

Keyboard

EscapeClose the drawer.
TabShift+TabCycle focus within the drawer (focus is trapped).
A1.3.1Info and RelationshipsA2.1.1KeyboardA2.4.3Focus OrderA4.1.2Name, Role, Value
  • 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.