Drawer
Direction-aware sheet (top, bottom, left, right) built on vaul with native drag-to-dismiss gestures.
Overview
Drawer is a vaul-backed overlay that slides in from any edge and supports native drag-to-dismiss with momentum. It is the right primitive for mobile contexts: quick actions, simple forms, action sheets. On md+ viewports prefer Dialog (focused decision) or Sheet (longer lateral flow), since desktop ergonomics don't reward bottom sheets.
The recommended pattern is responsive: Drawer below md, Dialog or Sheet above. Configure with the direction prop on Drawer itself.
Preview
'use client';import { Button } from '@gremorie/rx-forms';import { Drawer, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,} from '@gremorie/rx-overlays';export function DrawerPreview() { return ( <Drawer> <DrawerTrigger asChild> <Button variant="outline">Open drawer</Button> </DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Move to archive</DrawerTitle> <DrawerDescription> Archived items can be restored within 30 days. </DrawerDescription> </DrawerHeader> <DrawerFooter> <Button>Archive</Button> </DrawerFooter> </DrawerContent> </Drawer> );}Anatomy
Drawer vaul root holding open state + direction
├─ DrawerTrigger element that opens the drawer
└─ DrawerContent portalled panel with a drag handle (bottom)
├─ DrawerHeader wraps title + description
│ ├─ DrawerTitle accessible headline
│ └─ DrawerDescription accessible body text
└─ DrawerFooter bottom action row
└─ DrawerClose closes the drawerInstallation
bash npx gremorie@latest add rx-drawer bash pnpm dlx gremorie@latest add rx-drawer bash yarn dlx gremorie@latest add rx-drawer bash bunx --bun gremorie@latest add rx-drawer Usage
import { Button } from "@gremorie/rx-forms";
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@gremorie/rx-overlays";
export function Example() {
return (
<Drawer>
<DrawerTrigger asChild>
<Button variant="outline">Open drawer</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Move to archive</DrawerTitle>
<DrawerDescription>
Archived items can be restored within 30 days.
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<Button>Archive</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}The Angular edition of this component ships from source today (see the workbench for the side-by-side); its registry entry is coming next.
API
<Drawer>
Extends vaul Drawer.Root. Notable props:
| Prop | Type | Default | Description |
|---|---|---|---|
direction | "top" | "bottom" | "left" | "right" | "bottom" | Edge from which the drawer slides in. |
open | boolean | - | Controlled open state. |
defaultOpen | boolean | false | Initial open state when uncontrolled. |
onOpenChange | (open: boolean) => void | - | Fired when open state changes. |
shouldScaleBackground | boolean | - | Applies a subtle scale to siblings of the drawer (iOS look). Requires [vaul-drawer-wrapper] on a parent. |
snapPoints | (string | number)[] | - | Resting positions (e.g. [0.4, 0.8, 1]). |
activeSnapPoint | string | number | null | - | Controlled snap point. |
onAnimationEnd | (open: boolean) => void | - | Fired after drag/animation settles. |
modal | boolean | true | Disable outside interaction while open. |
All other props from vaul Drawer.Root are forwarded.
<DrawerContent>
Wraps vaul Drawer.Content with the overlay and a Portal. Layout adapts to direction:
bottom(default): docks to the bottom, top-rounded, shows the gripper handle.top: docks to the top, bottom-rounded.right/left: full-height side panel,w-3/4capped atsm:max-w-sm.
The horizontal gripper handle is rendered automatically for direction="bottom".
<DrawerHeader>, <DrawerFooter>, <DrawerTitle>, <DrawerDescription>
Layout containers with vaul-friendly defaults: header is centered for top/bottom directions and left-aligned at md+; footer pins to the bottom of the content.
<DrawerTrigger>, <DrawerClose>, <DrawerOverlay>, <DrawerPortal>
Pass-throughs over the vaul primitives with data-slot attributes.
Composition
<Drawer>owns open/close state and gesture handling. Setdirectionhere.<DrawerTrigger asChild>wraps the focusable element that opens it.<DrawerContent>mounts via Portal, draws the overlay, animates from the chosen edge.- Header holds the title and description; footer holds primary actions.
- Dismissal: drag past the threshold, click overlay, press
Esc, or callDrawerClose.
Variations
Directions
Set the direction prop on Drawer to slide the panel in from any edge (top, bottom, left, right). The gripper handle renders automatically for bottom.
'use client';import { Button } from '@gremorie/rx-forms';import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger,} from '@gremorie/rx-overlays';const directions = ['top', 'bottom', 'left', 'right'] as const;export function DrawerDirectionsPreview() { return ( <div className="flex flex-wrap gap-2"> {directions.map((direction) => ( <Drawer key={direction} direction={direction}> <DrawerTrigger asChild> <Button variant="outline" className="capitalize"> {direction} </Button> </DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle className="capitalize"> {direction} drawer </DrawerTitle> <DrawerDescription> This drawer slides in from the {direction} edge with native drag-to-dismiss. </DrawerDescription> </DrawerHeader> </DrawerContent> </Drawer> ))} </div> );}Bottom drawer with form
The canonical mobile pattern. Quick form, primary action in the footer.
<Drawer>
<DrawerTrigger asChild>
<Button>New note</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>New note</DrawerTitle>
<DrawerDescription>Add a quick note to this project.</DrawerDescription>
</DrawerHeader>
<div className="px-4">
<Textarea placeholder="Type something..." />
</div>
<DrawerFooter>
<Button>Save note</Button>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>Right side drawer
Switch to a side panel for desktop secondary navigation or filter trays.
<Drawer direction="right">
<DrawerTrigger asChild>
<Button variant="outline">Filters</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Filters</DrawerTitle>
</DrawerHeader>
{/* Filter controls */}
</DrawerContent>
</Drawer>Snap points
Resting positions let the drawer settle at 40% then expand to full height. Useful for previews that can be expanded.
const [snap, setSnap] = useState<number | string | null>(0.4);
<Drawer
snapPoints={[0.4, 1]}
activeSnapPoint={snap}
setActiveSnapPoint={setSnap}
>
<DrawerTrigger asChild>
<Button>Preview</Button>
</DrawerTrigger>
<DrawerContent>{/* content scales between 40% and 100% */}</DrawerContent>
</Drawer>;Accessibility
- Role:
role="dialog"witharia-modal="true". - Keyboard:
Esccloses;Tabcycles focus within the content; focus is trapped while open. - Focus management: focus moves into the drawer on open and returns to the trigger on close.
- Title:
DrawerTitleis required for screen-reader announcement (usesr-onlyif you need to hide it visually). - Gesture dismiss: drag-to-dismiss is keyboard-equivalent to
Esc. Users on assistive tech are not penalized. - Reduced motion: drag animations honor
prefers-reduced-motion; vaul falls back to instant transitions.