Sheet
A panel that stays open over a full-bleed canvas like a map: a floating card on a wide container, a bottom sheet with three detents on a narrow one. Unlike Drawer, it never blocks the page.
Installation#
Usage#
Put the Sheet inside the positioned element it floats over, usually the box your map fills. It is absolute in there, and that element is what it measures: wider than breakpoint (768px by default) it floats as a card, narrower it rises from the bottom. It follows the container, not the viewport, so it behaves the same full-screen and inside a panel.
import {
Sheet, SheetHandle, SheetHeader, SheetTitle,
SheetDescription, SheetBody, SheetFooter,
} from "@/components/ui/sheet"
export function MapScreen() {
return (
<div className="relative h-dvh">
<Map className="absolute inset-0" />
<Sheet>
<SheetHandle />
<SheetHeader>
<SheetTitle>Nearby</SheetTitle>
<SheetDescription>Five places open now.</SheetDescription>
</SheetHeader>
<SheetBody>{/* … */}</SheetBody>
<SheetFooter>{/* … */}</SheetFooter>
</Sheet>
</div>
)
}Detents#
On a narrow canvas the sheet rests at one of three heights: peek (the header and a row or two, peekHeight sets it), half, and full, which leaves a strip of the canvas visible at the top. Drag the handle or the header and let go: the release velocity is projected, so a flick carries it to the next detent. A tap on the handle steps it up. Control it with detent and onDetentChange, or let it start at defaultDetent.
Side and size#
The floating card docks to the left (the default) or right edge, top to bottom with a margin all round. size picks its width on the same steps Drawer uses (sm, md by default, lg, xl); a width class of your own replaces it. A bottom sheet always spans the canvas.
The visible area#
A map centred on the whole canvas puts half of what you look at under the sheet. onInsetChange reports how much of the canvas the sheet covers, per edge and in pixels, every time that changes (the layout, the detent, the width, and each frame of a drag). Hand it to the map as padding and it centres things in the part that is still visible.
Keyboard and screen readers#
The Sheet is a named <aside>, a complementary landmark, labelled by its SheetTitle (or pass an aria-label). Nothing traps focus and nothing behind it goes inert: it is part of the page. On a bottom sheet the handle is a focusable window splitter: ArrowUp and ArrowDown step between detents, Home and End jump to peek and full, Enter and Space step it up, and it announces the detent through aria-valuetext (rename them with detentLabels).
API reference#
Sheet renders an <aside> and adds detent, defaultDetent (peek | half | full, default half), onDetentChange, side (left | right), size (sm | md | lg | xl, default md), breakpoint (px, default 768), peekHeight (px, default 176), onInsetChange and density (comfortable | compact). It exposes data-layout (floating | bottom), data-detent and data-dragging for styling. SheetHandle takes detentLabels and renders nothing while the sheet floats. SheetHeader, SheetBody and SheetFooter are divs, SheetTitle an <h2> and SheetDescription a <p>; every part takes className. The useSheet() hook returns the layout, the detent and setDetent to anything inside.