Skip to content
Folio

Overlays

Sheet

A panel docked to an edge of the viewport.

Usage

When to reach for it

A modal that needs room — a filter panel, a detail view. It IS a Dialog, docked; the sides are named in reading order, so `end` is the right in English and the left in Arabic.
TSX
import { Sheet } from '@misoto22/design'

Notes

Radix Dialog root, trigger and close — a sheet IS a dialog, docked.

Anatomy

Sheet anatomy
ElementDescription
ScrimrequiredThe same --scrim at --z-overlay that Dialog uses — the same component, in fact, so the page behind is inert and scroll-locked exactly as it is under a dialog.
PanelrequiredThe docked box. start and end are a min(24rem, 92vw) column at full height; top and bottom are a full-width band capped at 85vh. A flex column that scrolls itself.
Titlerequiredtitle, typed as required rather than optional — there is no unnamed-sheet path to fall into, only a hidden-title one via hideTitle.
Descriptiondescription, under the title, sharing the wrapper that hideTitle hides.
CloserequiredThe X in the top-end corner. Unlike Dialog there is no showClose to turn it off, so every sheet has one whichever edge it is docked to.

Best practices

Do

  • Reach for a Sheet over a Dialog when the content is a list or a form long enough to scroll: it gets the full height of the viewport rather than Dialog’s 32rem by 85vh box, and the reader keeps the page edge as an anchor.
  • Use top or bottom when the content is wide and short — a filter bar, a date range. start and end are a 24rem column, and a table pushed into one wraps into a ribbon.
  • Name the sides start and end rather than reaching for left and right: each edge has its own literal class string carrying its own rtl: variant, so end arrives from the right in English and the left in Arabic with no second code path.
  • Wrap the cancelling control in SheetClose so the close runs through Radix — a sheet closed by your own state setter leaves focus inside a panel that is no longer on the page.

Don’t

  • Do not re-declare the travel in className. The panel carries data-m22-animated, which removes its transform outright under reduced motion; a second transform of your own only gets the universal floor, so it still arrives — a hundredth of a millisecond later, from wherever you put it.
  • An OverlayContainer whose element is not positioned docks the sheet to the wrong box: naming a container switches the panel from fixed to absolute, so an unpositioned container sends it to the nearest positioned ancestor rather than to the frame.
  • It is a modal dialog, so the page behind is scroll-locked and pointer-inert: this is not the home for a filter panel the reader is meant to work alongside. That is a Popover, or a column in the layout.

Examples

sides

The four edges, named in reading order rather than as left and right: end is the right in English and the left in Arabic, from one class string carrying its own rtl variant, so there is no second code path to keep in step. Flip this canvas to RTL and watch start and end swap. start and end are a min(24rem, 92vw) column at full height; top and bottom are a full-width band capped at 85vh. Every sheet has a close button whichever edge it comes from — there is no showClose to turn it off, unlike Dialog.

room to scroll

The case a Dialog cannot take: a filter list long enough to scroll. Docked to end, the panel gets the full height of the viewport rather than Dialog's 32rem by 85vh box, and the reader keeps the page edge as an anchor while the list moves. Note the labels: Checkbox renders none of its own, so the words — and the click target they give it — are the call site's job. Both footer controls are SheetClose: closing through Radix is what puts focus back on the trigger instead of leaving it inside a panel that is no longer on the page.

a wide band

Content that is wide and short belongs on the bottom edge, not in a column. start and end are a 24rem column, and a four-field row pushed into one wraps into a ribbon the reader scrolls; bottom is full width and capped at 85vh, so the row stays a row. It is still a modal dialog — the page behind is scroll-locked and pointer-inert — so this is for a range the reader sets and dismisses, not for a panel they work alongside. That one is a Popover, or a column in the layout.

Parts

Composed at the call site rather than configured through props, so a layout this component did not anticipate is still expressible.

SheetContent

A panel docked to an edge of the viewport.

It is a modal dialog — Radix's, so the focus trap, the escape key, the scroll lock and the aria-modal wiring are the same ones Dialog gets. The only differences are where it sits and which way it arrives, which is why this shares that implementation rather than reproducing it: a second focus trap is a second focus trap to get wrong.

The title is required, visible or not. A modal with no accessible name drops a screen reader into an unnamed region with no way back out.

Portals into the element an enclosing OverlayContainer names, docking to that element's edge rather than the viewport's when there is one.

SheetContent props
PropTypeDefaultDescription
titlerequiredReactNode
closeLabelstring'Close'Accessible close action, supplied by the host locale.
descriptionReactNode
hideTitlebooleanfalseHide the title visually while keeping it for assistive tech.
sideSheetSide'end'Which edge it is docked to. `end` by default.

Also accepts everything in Omit<ComponentProps<typeof DialogPrimitive.Content>, 'title'>. Those are forwarded to the underlying element and are not listed row by row.

Re-exports

Sheet = DialogPrimitive.Root

Radix Dialog root, trigger and close — a sheet IS a dialog, docked.

SheetTrigger = DialogPrimitive.Trigger
SheetClose = DialogPrimitive.Close

Types

TSX
export type SheetSide = 'start' | 'end' | 'top' | 'bottom'

Keyboard

Sheet keyboard interactions
KeyDoes
EscapeCloses it, and focus returns to the trigger.
TabCycles inside the sheet.

Accessibility

  • Shares Dialog’s focus trap, Escape handling and scroll lock rather than reproducing them — a second focus trap is a second one to get wrong.
  • The title is required, visible or not.