Morpha UI

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 drawer

Composition

<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:

VariableDefaultWhat it does
--drawer-inset12pxMargin between the drawer and the viewport edges. 0px makes it flush.
--drawer-bleed-backgroundsheet surfaceBackground of the strip that continues past the edge during an over-drag.
--drawer-overlay-min-opacity0Floor 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 drawer

API 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 DrawerTitle is required (wrap it in sr-only if you don't want it shown) so the drawer has an accessible name; DrawerDescription is wired to aria-describedby when 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 a DrawerClose action).
  • 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.

On this page