Skip to content

Drawer

Stable

A touch-first panel that slides from an edge, follows the finger, rests on snap points and dismisses with a swipe.

Installation

$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/drawer.json

Usage

import {
  Drawer,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from "@/components/ui/drawer"
<Drawer showSwipeHandle>
  <DrawerTrigger render={<Button variant="outline" />}>Set daily goal</DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Daily move goal</DrawerTitle>
      <DrawerDescription>Set your daily activity goal.</DrawerDescription>
    </DrawerHeader>
    {/* … */}
    <DrawerFooter>
      <DrawerClose render={<Button size="lg" />}>Save goal</DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

The drawer is built on the Base UI Drawer: the panel tracks the pointer while it's dragged, dismisses when the gesture is fast or far enough, and settles on the nearest resting point instead of stopping wherever the finger let go.

Examples

Bottom sheet

The default: a drawer that rises from the bottom and is dismissed by swiping down. showSwipeHandle renders the grab handle that tells people it can be dragged. Center the content with a max-w-* wrapper so it stays readable on tablets — a share sheet is the archetype.

Snap points

snapPoints lets the drawer rest at several heights. Fractions between 0 and 1 are portions of the viewport height; numbers above 1 are pixels, and strings accept px and rem. The drawer opens at the first point; drag it up to reach the next one.

<Drawer showSwipeHandle snapPoints={[0.45, 1]}>

Directions

swipeDirection sets both the edge and the dismiss gesture: down (default), up, left or right. Side drawers are 75% wide on phones and 24rem from sm up.

Nested

Open a drawer from inside another and the parent steps back into a stack — slightly smaller, slightly dimmed — so people see where they'll return. Swiping the child away brings the parent forward again.

Responsive

On desktop, a centered Dialog is easier to reach with a pointer; on phones, a bottom drawer is within thumb reach. Share the content and switch the container with useIsMobile.

const isMobile = useIsMobile()

return isMobile ? (
  <Drawer showSwipeHandle>{/* … */}</Drawer>
) : (
  <Dialog>{/* … */}</Dialog>
)

Guidelines

When to use

  • For contextual tasks on touch devices: pickers, short forms, share sheets, filters.
  • When the content benefits from resting at several heights, like a map with a list of places.

When not to use

  • On pointer-first desktop layouts — use a Dialog or a Sheet.
  • For destructive confirmations — use an Alert Dialog, which can't be swiped away by accident.
  • For long, complex forms — give them a page.

Gestures are a shortcut, never the only way

Everything a gesture does must also be possible with a tap or a key. Keep a visible close or cancel action in the footer, and don't rely on swiping between snap points to reveal essential content.

Share album24 photos
Do.A visible action closes the drawer; the swipe is a shortcut.
Share albumSwipe down to close
Don’t.Hidden gestures are invisible to screen reader and keyboard users.

Thumb reach

Put primary actions at the bottom of the drawer, full width, in the lg size (40px tall). That's where thumbs are, and it clears the 24px minimum target comfortably.

Accessibility

The drawer is a modal dialog: it traps focus, locks page scroll, and is labeled by DrawerTitle and described by DrawerDescription.

KeyBehavior
EnterSpace
On the trigger: opens the drawer and moves focus into it.
Tab
Moves through the drawer's controls. Focus is trapped while it's open.
ShiftTab
Moves focus backwards.
Esc
Closes the drawer and returns focus to the trigger.
  • Always include a title. It names the drawer for assistive technology; hide it with sr-only if the design has none.
  • The swipe handle is decorative and hidden from assistive technology — provide a real close button.
  • Motion. The drawer follows the pointer 1:1 while dragging and settles over 450ms. With reduced motion, the settle animation is removed; dragging still tracks the finger, because that motion is caused by the person, not by the interface.

API reference

Drawer

The root. Doesn't render an element. Accepts every prop of Base UI Drawer.Root.

PropTypeDefault
swipeDirection

The edge the drawer is attached to, and the swipe that dismisses it.

"down" | "up" | "left" | "right""down"
snapPoints

Resting positions. 0–1 is a fraction of the viewport height, > 1 is pixels, strings accept px and rem.

(number | string)[]No default
snapPoint

The active snap point. Use with onSnapPointChange to control it.

number | string | nullNo default
defaultSnapPoint

The initial snap point, when uncontrolled.

number | string | nullNo default
onSnapPointChange

Called when the drawer settles on another snap point.

(snapPoint, details) => voidNo default
snapToSequentialPoints

Moves one snap point at a time instead of skipping on fast swipes.

booleanfalse
showSwipeHandle

Renders the grab handle. prfct-specific.

booleanfalse
open

Whether the drawer is open. Use with onOpenChange to control it.

booleanNo default
defaultOpen

Whether it is open initially, when uncontrolled.

booleanfalse
onOpenChange

Called when it opens or closes. details.reason is "swipe" for gestures.

(open: boolean, details) => voidNo default
modal

true locks scroll and blocks the page; false keeps the page interactive.

boolean | "trap-focus"true
disablePointerDismissal

Prevents closing when clicking the scrim.

booleanfalse

DrawerContent

Renders the portal, scrim, viewport and panel. Accepts every prop of Base UI Drawer.Popup, including initialFocus and finalFocus.

DrawerHeader, DrawerFooter

DrawerHeader holds the title and description — centered on vertical drawers below md, left-aligned above. DrawerFooter stacks actions full width at the bottom.

DrawerTitle, DrawerDescription

DrawerTitle renders an <h2> and names the drawer. DrawerDescription renders a <p> and describes it.

DrawerTrigger, DrawerClose

Open and close the drawer. Both render a <button>; use render to render a prfct Button.