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:

  • DrawerContent automatically includes the portal, overlay, and drag handle — no extra wrappers needed.
  • The drag handle is only visible for direction="bottom".
  • Use DrawerHeader and DrawerFooter for 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 / Space on the handle toggles snap points or closes the drawer.
  • Arrow keys navigate between snap points when the handle is focused.
  • Escape closes 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; use right for 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.