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>
)
}Anatomy#
import {
Sheet,
SheetHandle,
SheetHeader,
SheetTitle,
SheetDescription,
SheetBody,
SheetFooter,
} from "@/components/ui/sheet"
<Sheet>
<SheetHandle />
<SheetHeader>
<SheetTitle />
<SheetDescription />
</SheetHeader>
<SheetBody>...</SheetBody>
<SheetFooter>...</SheetFooter>
</Sheet>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
An absolute <aside> that measures its parent: at or above breakpoint it floats as a card, below it rises as a bottom sheet with three detents. Forwards all native <aside> props, and every part accepts className, merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| detent | "peek" | "half" | "full" | - | The bottom sheet's detent (controlled). |
| defaultDetent | "peek" | "half" | "full" | "half" | The detent it starts at when uncontrolled. |
| onDetentChange | (detent: SheetDetent) => void | - | Called when a drag, tap, key or setDetent moves the detent. |
| side | "left" | "right" | "left" | The edge the floating card docks to. |
| size | "sm" | "md" | "lg" | "xl" | "md" | The floating card's width on the container scale; a bottom sheet always spans the canvas. |
| breakpoint | number | 768 | Canvas width (px) at and above which the sheet floats. |
| peekHeight | number | 176 | Height (px) of the peek detent: the header and a row or two. |
| onInsetChange | (insets: SheetInsets) => void | - | Reports how much of the canvas the sheet covers per edge (top, right, bottom, left, in px), on every layout, detent or width change and every drag frame. |
| density | "compact" | "comfortable" | "compact" | Padding and title size of the parts. Falls back to the nearest DensityProvider and passes it on to the content. |
| Data attribute | Description |
|---|---|
| data-layout | floating or bottom; absent for the first frame, while the canvas is measured (the sheet stays invisible). |
| data-detent | The current detent, on a bottom sheet only. |
| data-dragging | Present while a finger or pointer is dragging the sheet. |
| data-side | left or right. |
SheetHandle
The bottom sheet's grab bar and keyboard control, a focusable role="separator": ArrowUp and ArrowDown step detents, Home and End jump to the ends, Enter, Space or a tap step it up. Renders nothing while the sheet floats; forwards all native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| detentLabels | Partial<Record<SheetDetent, string>> | { peek: "Peek", half: "Half height", full: "Full height" } | What the handle announces for each detent as its aria-valuetext, for your language. |
| aria-label | string | "Resize the sheet" | The handle's accessible name. |
SheetHeader
The title block. On a bottom sheet it is also a drag region, so the whole top can be grabbed. Forwards all native <div> props.
SheetTitle
An <h2>; the sheet is labelled by it unless the Sheet gets an aria-label or aria-labelledby. Forwards all native <h2> props.
SheetDescription
Muted supporting text under the title. Forwards all native <p> props.
SheetBody
The scrollable content between header and footer. Forwards all native <div> props.
SheetFooter
A row pinned to the sheet's bottom edge, the place for a tab bar or the primary action. A lone child fills it; give several flex-1 to share it. Forwards all native <div> props.
useSheet
A hook for anything inside the Sheet: returns { layout, detent, setDetent }, where layout is "floating", "bottom" or null before the first measure.