Gesso
Overlays

Drawer

A bottom sheet you can grab, resting only at the heights you give it.

A bottom sheet for phones. It rests only at its detents: drag the handle or the header between them, flick to go to the next one, or drag below the smallest to dismiss. It sits on the same Radix dialog as Sheet, so focus, Escape, and the scroll lock behave the same, and the motion is a spring that carries the speed of your release.

Closed

Usage

import { Button } from '@/components/ui/button';
import {
  Drawer,
  DrawerBody,
  DrawerClose,
  DrawerContent,
  DrawerDescription,
  DrawerFooter,
  DrawerHeader,
  DrawerTitle,
  DrawerTrigger,
} from '@/components/ui/drawer';

<Drawer detents={[0.52, 0.9]}>
  <DrawerTrigger asChild>
    <Button>Nearby</Button>
  </DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Nearby</DrawerTitle>
      <DrawerDescription>15 places within a short walk</DrawerDescription>
    </DrawerHeader>
    <DrawerBody>{/* a long list */}</DrawerBody>
    <DrawerFooter>
      <DrawerClose asChild>
        <Button>Show on map</Button>
      </DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>

Detents

detents are the only heights the drawer rests at, smallest first. A number up to 1 is a fraction of the visible viewport and a number above it is pixels, so [0.25, 0.52, 0.9] is a peek, a half, and a full sheet, and [320, 0.9] mixes the two. The largest always leaves 24px of page above it. It defaults to Washi's pair, [0.52, 0.9].

A drag settles on the nearest detent, and a flick faster than 0.5px/ms goes to the next one in its direction. Past the largest detent the sheet stretches a little and springs back. Below the smallest it closes, or stretches the same way when dismissible is off.

Every detent keeps the whole sheet usable: the body and the footer end at the visible bottom edge, not at the bottom of the full-height sheet, so the last row of a list and the footer's action are reachable at the smallest detent too.

Moving it from code

Control the detent with detent and onDetentChange, or call useDrawer() inside the drawer. Opening a detail is the usual reason: Washi's Artists sheet goes full height when you pick a teammate and back to half when you step out.

function PlaceRow({ place }: { place: Place }) {
  const { setDetent, detents } = useDrawer();
  return (
    <button type="button" onClick={() => setDetent(detents.length - 1)}>
      {place.name}
    </button>
  );
}

An uncontrolled drawer opens at defaultDetent every time, which is the smallest unless you say otherwise.

Scrolling and dragging

The handle and the header always drag. DrawerBody scrolls, and hands the gesture to the sheet in two cases: pulling down while the body is at its top, and pushing up while a larger detent is left. The first move decides, since a scroll that has started cannot be taken back, so a sideways swipe on a carousel in the body stays a swipe. Content outside DrawerBody does not drag.

modal is on by default: a scrim dims the page, which cannot scroll or take focus, and the scrim fades as you drag toward closing. With modal={false} there is no scrim, the page stays usable, and tapping it leaves the drawer open, which is the shape of a Maps-style panel that lives beside the content.

Props

PropTypeDefaultWhat it does
detentsnumber[][0.52, 0.9]Resting heights, smallest first. Up to 1 is a viewport fraction, above it pixels.
detentnumberThe current detent's index, controlled
defaultDetentnumber0The detent it opens to
onDetentChange(index) => voidA drag, a flick, the handle, or code moved it
open / defaultOpen / onOpenChangeAs on Dialog
dismissiblebooleantrueDrag below the smallest detent, tap the scrim, or press Escape to close. Off, only DrawerClose or code closes it.
modalbooleantrueDim and lock the page behind it

DrawerContent takes showHandle, on by default. DrawerHeader, DrawerBody, and DrawerFooter are the header that drags, the region that scrolls, and the actions pinned to the visible bottom edge.

Accessibility

  • The handle is a button. Pressing it steps to the next detent and back down from the largest, ↑ and ↓ step one at a time, and Home and End jump to the ends. Its name says which way a press goes, "Expand sheet" or "Collapse sheet".
  • DrawerTitle names the dialog and DrawerDescription describes it, as on Dialog.
  • Focus moves into the drawer on open and back to the trigger on close. With modal on, the page behind is inert.
  • The sheet stays clear of the home indicator's safe area and rises above a soft keyboard.
  • Reduced motion moves between detents without the spring.

When to use something else

On a wide screen a bottom sheet is a long reach; use Dialog or Sheet there, and Drawer on phones. It caps its width at max-w-lg and centers itself, so it still reads as a sheet if it does open on a tablet.

Install

shadcn add @gesso/drawer

On this page