Drawer
A draggable panel that slides in from any edge of the screen.
Drawer
Drawer is a draggable overlay panel that slides in from any edge of the screen. Unlike Sheet, it supports touch/pointer drag gestures, snap points, and momentum-based dismissal — making it ideal for mobile-friendly experiences.
Drawer wraps the @loke/ui/drawer primitive and applies design system tokens. For unstyled usage, use the primitive directly.
Features
- Drag to dismiss with velocity-based threshold
- Four directions (bottom, top, left, right)
- Snap points for multi-height drawers
- Momentum damping when dragging past open state
- Optional non-dismissible mode
- Accessible keyboard navigation via the drag handle
- Built on Dialog primitives (focus trap, portal, overlay)
Usage
import {
Drawer,
DrawerTrigger,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerDescription,
DrawerFooter,
DrawerClose,
} from '@loke/design-system/drawer';
export default function Example() {
return (
<Drawer>
<DrawerTrigger asChild>
<Button variant="outline">Open</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Title</DrawerTitle>
<DrawerDescription>Description goes here.</DrawerDescription>
</DrawerHeader>
<div className="p-4">{/* content */}</div>
<DrawerFooter>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}Tips:
DrawerContentautomatically includes the portal, overlay, and drag handle — no extra wrappers needed.- The drag handle is only visible for
direction="bottom". - Use
DrawerHeaderandDrawerFooterfor consistent internal structure.
Props
Drawer
Prop
Type
DrawerContent
Prop
Type
Examples
Basic drawer
<Drawer>
<DrawerTrigger asChild>
<Button variant="outline">Open Drawer</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Edit Profile</DrawerTitle>
<DrawerDescription>Make changes here.</DrawerDescription>
</DrawerHeader>
<div className="grid gap-4 p-4">
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue="John Doe" />
</div>
<DrawerFooter>
<Button type="submit">Save changes</Button>
<DrawerClose asChild>
<Button variant="outline">Cancel</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>Directions
<Drawer direction="left">
<DrawerTrigger asChild>
<Button variant="outline">Left</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Left Drawer</DrawerTitle>
</DrawerHeader>
</DrawerContent>
</Drawer>Snap points
const snapPoints = [0.4, 0.75, 1];
const [activeSnapPoint, setActiveSnapPoint] = useState(snapPoints.at(-1));
<Drawer
snapPoints={snapPoints}
activeSnapPoint={activeSnapPoint}
setActiveSnapPoint={setActiveSnapPoint}
>
<DrawerTrigger asChild>
<Button variant="outline">Open with Snap Points</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Snap Points</DrawerTitle>
<DrawerDescription>Drag to snap between heights.</DrawerDescription>
</DrawerHeader>
</DrawerContent>
</Drawer>Non-dismissible
const [open, setOpen] = useState(false);
<Drawer open={open} onOpenChange={setOpen} dismissible={false}>
<DrawerTrigger asChild>
<Button variant="outline">Open Non-Dismissible</Button>
</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Confirm Action</DrawerTitle>
<DrawerDescription>
Cannot be dismissed by drag or outside click.
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<Button variant="destructive" onClick={() => setOpen(false)}>Confirm</Button>
<Button variant="outline" onClick={() => setOpen(false)}>Cancel</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>Accessibility
- Drag handle has
role="separator"and is keyboard-focusable. Enter/Spaceon the handle toggles snap points or closes the drawer.- Arrow keys navigate between snap points when the handle is focused.
Escapecloses the drawer.- Focus is trapped within the drawer when open.
- Screen reader announces title and description.
- Focus returns to the trigger on close.
Best practices
- Default to
direction="bottom"for mobile-first flows; userightfor detail panels on desktop. - Use snap points when the drawer has variable content density (e.g. a map + list).
- Set
dismissible={false}only for destructive confirmations that require explicit intent. - Always include a
DrawerTitle— even if visually hidden — for screen reader context. - Avoid deeply nested drawers; use
DrawerNestedRoot(from@loke/ui/drawer) for true nesting.