Drawer
A swipeable sheet that slides in from an edge of the screen.
Overview
A drawer is the Material-3 sheet: a surface that slides in from an edge of the
viewport and is dismissed by swiping it back out. It is built on the Base UI
Drawer primitive — swipe-to-dismiss, snap points, nested stacking, focus trap and
scroll-lock come from the primitive — and styled to the kit's sheet surface:
surface-container-low, 28px corners, a hairline outline, the elevation-3 shadow
and a scrim/32 backdrop whose opacity tracks the swipe. The sheet floats: it
sits 12px in from the viewport edges, so all four corners are rounded. Set
--drawer-inset to 0px for the flush, edge-to-edge sheet (and square the outer
corners with it — see Styling).
The direction it enters from is the direction you swipe it away: swipeDirection
(default down) makes it a bottom sheet, a side sheet or a top sheet.
$ npx morpha-ui add drawerComposition
<Drawer swipeDirection="down" showSwipeHandle>
<DrawerTrigger render={<Button variant="tonal">Open</Button>} />
<DrawerContent>
<DrawerHeader>
<DrawerTitle />
<DrawerDescription />
</DrawerHeader>
<DrawerBody /> {/* optional — the scrolling region */}
<DrawerFooter>
<DrawerClose render={<Button variant="text">Close</Button>} />
</DrawerFooter>
</DrawerContent>
</Drawer>DrawerContent renders the portal, the backdrop, the viewport and the popup for
you. DrawerOverlay, DrawerPortal and DrawerSwipeHandle are exported too, for
the rare case you need to assemble the surface yourself.
Examples
Custom sizes
A drawer is as tall as its content. To pin it, set the size on the axis it travels
on: h-* for a vertical drawer, w-* for a horizontal one. Side drawers are 75%
wide on mobile and 24rem from sm up.
Position
swipeDirection takes down, up, left or right. The corners, the border
and the enter transition all follow the edge, and the side directions are
logical — under dir="rtl" left and right mirror with the document.
Swipe handle
showSwipeHandle renders the MD3 drag handle (32×4px, on-surface-variant at
40%) on the swiped edge. It is the affordance that tells a touch user the surface
can be dragged away — the whole surface stays draggable either way.
Nested
Drawers stack. The parent stays mounted, scales back and dims behind the frontmost drawer, then comes forward again when it closes.
Non modal
modal={false} drops the scrim and the scroll lock: the page keeps scrolling and
stays clickable with the drawer open. disablePointerDismissal additionally keeps
it open when you click outside. modal="trap-focus" is the middle ground — focus
stays inside the drawer, the page still scrolls.
Snap points
With snapPoints the drawer fills the viewport and rests at the heights you list:
a number below 1 is a fraction of the viewport, a number above 1 a pixel value,
and a string takes px / rem. Control the active one with snapPoint and
onSnapPointChange; the scrim never fades below 50% while snapped.
Responsive
The "sheet on mobile, dialog on desktop" pattern: the same content, two surfaces,
picked from the viewport width with useIsMobile (768px).
Styling
The popup reads a few CSS variables you can set from className:
| Variable | Default | What it does |
|---|---|---|
--drawer-inset | 12px | Margin between the drawer and the viewport edges. 0px makes it flush. |
--drawer-bleed-background | sheet surface | Background of the strip that continues past the edge during an over-drag. |
--drawer-overlay-min-opacity | 0 | Floor for the scrim opacity while swiping. Set to 0.5 automatically when snap points are used. |
For the flush, edge-to-edge sheet, drop the inset and square the corners that no longer face the content — otherwise the page shows through the gaps:
<DrawerContent className="rounded-none rounded-t-[28px] [--drawer-inset:0px]" />That is exactly what the mobile navigation rail does with its off-canvas panel.
State lives on data attributes: data-swipe-direction, data-swipe-axis,
data-swiping, data-snap-points, data-nested-drawer-open, plus the usual
data-starting-style / data-ending-style for the enter and exit transitions.
Coming from shadcn? This replaces both drawer (vaul) and sheet. There is
no direction prop — it is called swipeDirection, and the drag handle is opt-in
through showSwipeHandle rather than bottom-only by default.
Installation
Morpha UI components are distributed via the Morpha CLI: npx morpha-ui add drawer.
npx morpha-ui add drawerAPI Reference
The Drawer root adds showSwipeHandle; every Base UI Drawer.Root prop is
forwarded (swipeDirection, modal, snapPoints, snapPoint,
onSnapPointChange, disablePointerDismissal, open / onOpenChange, …).
Prop
Type
DrawerBody — the scrolling region — takes one extra prop:
Prop
Type
DrawerTrigger, DrawerContent, DrawerClose, DrawerTitle and
DrawerDescription forward their Base UI props; DrawerHeader and DrawerFooter
forward div props.
Accessibility
- A
DrawerTitleis required (wrap it insr-onlyif you don't want it shown) so the drawer has an accessible name;DrawerDescriptionis wired toaria-describedbywhen present. - Focus is trapped while a modal drawer is open and restored to the trigger on close; Esc dismisses it.
- The drag handle is
aria-hidden— it is a touch affordance, not a control. Every drawer must still be dismissible from the keyboard (Esc, or aDrawerCloseaction). - Swipe gestures are pointer-based and never the only way out; the transitions respect the swipe velocity rather than running a fixed animation.
The drawer is rendered in a portal on document.body, so inside the preview it
follows the site theme rather than the frame's light/dark toggle.