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

proptypedefault
as?ElementType"div"the element or component to render
side?SheetSide"end"the edge it anchors to and slides from
className?stringmerged over the defaults, so it wins
…restprops of astyped against the chosen element

Sheet.Body

The only part that scrolls: it takes the space the header and the footer leave.

proptypedefault
as?ElementType"div"the element or component to render
className?stringmerged over the defaults, so it wins
…restprops of astyped against the chosen element

Sheet.Footer

The actions, aligned to the end and separated by a rule.

proptypedefault
as?ElementType"div"the element or component to render
className?stringmerged over the defaults, so it wins
…restprops of astyped 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.

tsx
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>
previewEsc and the backdrop close them

Details

Sides

side
endthe default: full height on the trailing edge
startfull height on the leading edge, for navigation
topfull width from the top, capped at 40% of the screen
bottomfull 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.

tsx
<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.

css
.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.