Sheet
A panel anchored to an edge of the screen, sliding in from it. The same engine as a dialog, a different place to arrive from.
API
Props
| prop | type | default | |
|---|---|---|---|
as? | ElementType | "div" | the element or component to render |
side? | SheetSide | "end" | the edge it anchors to and slides from |
className? | string | — | merged over the defaults, so it wins |
…rest | props of as | — | typed against the chosen element |
Sheet.Body
The only part that scrolls: it takes the space the header and the footer leave.
| prop | type | default | |
|---|---|---|---|
as? | ElementType | "div" | the element or component to render |
className? | string | — | merged over the defaults, so it wins |
…rest | props of as | — | typed against the chosen element |
Sheet.Footer
The actions, aligned to the end and separated by a rule.
| prop | type | default | |
|---|---|---|---|
as? | ElementType | "div" | the element or component to render |
className? | string | — | merged over the defaults, so it wins |
…rest | props of as | — | typed against the chosen element |
Examples
Usage
A sheet is composed exactly like a dialog: Layer for the behaviour, the panel for the surface, Header on top. Only the panel changes, and with it where the thing comes from.
const { close, id, isActive, open } = useGraftDialog<void>();
<Layer open={isActive} onClose={close} aria-labelledby={id}>
<Sheet side="end">
<Header className="border-b-2 border-border">
<Header.Title id={id} title="Filters" />
<Header.Trailing>
<Button aria-label="close" onClickBlur={close}>
<Button.Icon icon={<MdClose />} />
</Button>
</Header.Trailing>
</Header>
<Sheet.Body>…</Sheet.Body>
<Sheet.Footer>
<Button onClickBlur={close}>
<Button.Label label="apply" />
</Button>
</Sheet.Footer>
</Sheet>
</Layer>Details
Sides
| side | |
|---|---|
end | the default: full height on the trailing edge |
start | full height on the leading edge, for navigation |
top | full width from the top, capped at 40% of the screen |
bottom | full width from the bottom, the usual mobile sheet |
The side decides three things at once: which edge the panel sticks to, which border it draws, and the direction it slides from. A sheet on start or end takes the full height and is capped in width; one on top or bottom takes the full width and is capped in height.
Size
The caps are defaults, not decisions: 26rem on the horizontal sides and 40% of the screen on the vertical ones. They are ordinary classes, so a wider panel or a shorter bottom sheet is a className — and the radius on the free edges is yours to add.
<Sheet side="end" className="max-w-[40rem]" />
<Sheet side="bottom" className="max-h-[60%] rounded-t-lg" />How it slides
The panel sits translated off its edge, and the rule that brings it back is scoped to an open dialog. So the entry and the exit are the same declaration read in two directions, and both ride the layer's own duration.
.margo-sheet[data-margo-side="end"] {
translate: 100% 0;
}
dialog[open] .margo-sheet {
translate: 0 0;
}Because the translation is a transform, it stays on the compositor and does not touch layout — and it is disabled with everything else under prefers-reduced-motion, where the sheet simply appears.
A sheet only travels: it never fades, because a panel arriving from an edge already says it is arriving, and a simultaneous fade makes it read as half-rendered. The fade belongs to the backdrop, and to panels that stay put — a Dialog has nowhere to arrive from, so fading is all it can do.
The layer clips its own overflow for the same reason. A dialog scrolls its content by default, so a panel parked off the right or the bottom edge would turn the layer into a scroll area — and the browser, moving focus into the sheet as it opens, would scroll it back into view and eat the entrance.
Sheet or Dialog
A dialog interrupts: it takes the centre and asks for an answer before you go on. A sheet extends: it comes from an edge, leaves the page visible behind it, and holds things you work through — filters, a detail panel, a navigation drawer.
Both are modal here, so both trap focus and lock the page. When the content should not interrupt at all, what you want is not a sheet but a panel in the layout.