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.
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 or not
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
| Prop | Type | Default | What it does |
|---|---|---|---|
detents | number[] | [0.52, 0.9] | Resting heights, smallest first. Up to 1 is a viewport fraction, above it pixels. |
detent | number | The current detent's index, controlled | |
defaultDetent | number | 0 | The detent it opens to |
onDetentChange | (index) => void | A drag, a flick, the handle, or code moved it | |
open / defaultOpen / onOpenChange | As on Dialog | ||
dismissible | boolean | true | Drag below the smallest detent, tap the scrim, or press Escape to close. Off, only DrawerClose or code closes it. |
modal | boolean | true | Dim 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".
DrawerTitlenames the dialog andDrawerDescriptiondescribes it, as on Dialog.- Focus moves into the drawer on open and back to the trigger on close. With
modalon, 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