Drawer
A dialog anchored to an edge of the screen, for panels rather than questions.
Installation
npx shadcn@latest add https://qure-ui.qure.ai/r/drawer.jsonUsage
import {
Drawer, DrawerClose, DrawerContent, DrawerDescription,
DrawerFooter, DrawerHeader, DrawerScroll, DrawerTitle, DrawerTrigger,
} from '@/components/ui/drawer'<Drawer side="right">
<DrawerTrigger render={<Button variant="secondary">Study details</Button>} />
<DrawerContent>
<DrawerHeader>
<DrawerTitle>CT Thorax — ACC-4471902</DrawerTitle>
</DrawerHeader>
</DrawerContent>
</Drawer>When to use it instead of Dialog
A Dialog asks a question and expects an answer. A drawer holds a panel: a study's details beside the images, filters over a worklist, a form long enough that a centred box would fight the page for vertical space.
The surface, the scrim and the shadow are Dialog's on purpose — a drawer that looked like a different product would be one. What differs is where it comes from, that it can be swiped away, and that it can be left open beside a live page.
Use Alert Dialog when the answer is destructive; a drawer is much too easy to dismiss for that.
Composition
<Drawer side="right"> {/* owns open state; picks the swipe direction from side */}
<DrawerTrigger /> {/* usually render={<Button/>} */}
<DrawerContent> {/* backdrop + viewport + popup + Base UI's Content */}
<DrawerHeader> {/* title and description, fixed */}
<DrawerTitle />
<DrawerDescription />
</DrawerHeader>
<DrawerScroll /> {/* the part that scrolls, if there is one */}
<DrawerFooter /> {/* actions, fixed to the bottom */}
</DrawerContent>
</Drawer>DrawerContent renders Base UI's Drawer.Content inside the popup for you. That part is not
decoration: it is what lets a mouse select text in the panel without the drag being read as a
swipe. A drawer built without it feels subtly broken in a way that is hard to trace.
DrawerScroll takes whatever height is left after the header and footer, so the title stays
legible and the Save button never scrolls out of reach.
Sides
side is ours, not Base UI's — the primitive has no placement prop at all, because placement is
entirely CSS. What it does have is swipeDirection, and the two must agree or a drawer on the
left is dismissed by dragging it further onto the screen. Drawer derives one from the other, so
setting side is enough.
Right is the default and the right default: it is where a details panel goes next to content the reader is still looking at. Bottom is for touch. Top is for something arriving unbidden, which is usually a Toast instead.
A form in a drawer
The case a drawer earns over a dialog. Four fields and a textarea in a centred box either overflow the viewport or squeeze the content; against the edge there is a full screen height to work with.
A bottom sheet with snap points
snapPoints lets the sheet rest part-open. Fractions are of the viewport height, numbers above
1 are pixels, and strings like '320px' or '30rem' are taken as written. Here 0.45 shows the
first few filters while the worklist behind stays visible, and dragging up goes to full height.
The grabber bar appears automatically on the top and bottom edges, where the affordance is a
drag rather than a click. It is aria-hidden and has no behaviour of its own — it is a mark, not
a control. Override with showGrabber.
Non-modal
modal={false} leaves the page underneath live: no focus trap, no scroll lock, and clicks pass
through the area outside the panel. This is what a viewer sidebar wants — a radiologist reading
the panel and scrolling the images at once.
modal="trap-focus" is the middle setting: focus stays inside, but the page still scrolls.
A non-modal drawer tells assistive technology nothing about the rest of the page still being
available. Keep it short, always give it a DrawerTitle, and do not use it for anything the
user must finish.
Dismissal, tested
Pressing Escape closes the drawer. Clicking the scrim closes it. Dragging it towards its own edge
past roughly half its width closes it, and releasing short of that springs it back — the scrim
lightens with the drag, because Base UI publishes the gesture's progress as
--drawer-swipe-progress and the stylesheet reads it.
disablePointerDismissal keeps the scrim click from closing it. Escape still works, and there is
no prop that stops it — the same as Alert Dialog, and correct for the same reason: a dialog with
no keyboard exit is a trap.
Two named traps
Drawer.Handle is not the grabber bar. In Base UI it is the object that lets a trigger
rendered elsewhere on the page open a drawer defined somewhere else — the equivalent of
Preview Card's handle. It is re-exported here as createDrawerHandle to keep the two apart.
The grabber is drawn by DrawerContent.
Drawer.Provider, Drawer.Indent and Drawer.IndentBackground in Base UI's anatomy are for
the iOS pattern where the page behind scales back as the sheet rises. DrawerContent does not
render them; add them yourself around the root if you want that effect.
API Reference
Everything Base UI's Drawer accepts. The ones you will reach for, plus ours:
Prop
Type
On DrawerContent:
Prop
Type