Sheet
Hero example
When to use
Reach for Sheet when the surface needs the "blurred-background + floating panel" treatment — endpoint detail in the API explorer, action confirmation, contextual zoom on a record. Sheet is the canonical Matter overlay — when you need a modal-style focused surface but don't need a strict modal contract (focus trap, escape stack management, ARIA live regions), Sheet is the right primitive.
Don't reach for Sheet when you need WAI-ARIA dialog semantics with focus trap and stacked-modal behaviour — use Dialog from @matter/components/headless (Radix-backed). Don't use Sheet for navigation either; the overlay implies "I'll be done here in a moment," not "I'm a new page."
Anatomy
Three structural pieces, portaled to document.body:
- Scrim. Full-viewport overlay.
role="dialog",aria-modal="true". Backdrop click triggersonClose. - Card. Centered floating panel. Composes
.sheet-card.glass— the Glass surface with a top specular highlight and bottom edge shadow.tintprop adjusts the background tint via--sheet-card-tint.widthdefaults tomin(560px, 100%). - Close button. Top-right
×button. Rendered whenshowCloseistrue(default). Always carriesaria-label="Close".
The sheet portal mounts only on the client (mounted flag guards SSR). Escape is bound globally while open is true; the listener unbinds on unmount.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| openrequired | boolean | — | — |
| onClose | () => void | — | — |
| tint | string | "20, 17, 13" | — |
| width | number | string | "min(560px, 100%)" | — |
| showClose | boolean | true | — |
| children | React.ReactNode | — | — |
tint is the CSS-rgb-triplet (no rgb() wrapper) used as --sheet-card-tint. The default "20, 17, 13" is the canonical Matter ink. Pass a custom triplet to brand the sheet to a specific lifecycle phase (e.g. "47, 122, 110" for the create-action green).
States
| State | Trigger | Behaviour |
|---|---|---|
| Closed (default) | open: false | Returns null; nothing in the DOM |
| Open | open: true | Scrim and card mount via portal; Escape listener binds |
| Closing | onClose fires | Consumer is responsible for setting open: false; the sheet doesn't animate-out internally |
There is no "loading" state — load content inside the card and render a <CardSkeleton> placeholder if needed.
Density
Density-agnostic — Sheet is a hero-level overlay. The card's internal padding inherits from whatever the consumer renders inside (typically a <Card> or hand-authored layout).
Themes
In dark, the scrim darkens further to maintain contrast against the underlying dark surface. In forced-colors, the scrim collapses to a semi-transparent Canvas and the card to Canvas with ButtonBorder.
Tokens consumed
Scrim background reads --ink-50. Card surface composes the Glass recipe (--surface-glass, --surface-glass-border, top highlight, bottom shadow). Card radius reads --radius-lg.
Accessibility
- Keyboard interactions. Escape closes (global listener). There is no focus trap — consumers managing focus inside the sheet must handle Tab cycling themselves, or use Dialog from
@matter/components/headlessfor the trap-enabled version. - ARIA roles and properties.
role="dialog"+aria-modal="true". Consumer must supplyaria-labelledbypointing to a heading inside the card. Without it, screen readers announce only "dialog." - Focus order. Consumer responsibility. Best practice: on open, move focus to the first focusable element inside the card. On close, restore focus to the trigger.
- Screen-reader expectations. With
aria-labelledbywired, the sheet announces as "{heading text}, dialog." Without it, just "dialog." - Reduced motion. Scrim fade-in and card scale-in transitions collapse to instant under
prefers-reduced-motion: reduce. - Forced colors. Scrim → semi-transparent
Canvas; card →CanvaswithButtonBorder. Close button →ButtonTextonButtonFace. - WIG rules.
overscroll-behavior: containon the scrim (WIG touch rule prevents pull-to-refresh during a sheet). Real<button type="button">for close (semantic-element rule).aria-label="Close"on icon-only close button (icon-only-button rule).
Do / Don't
onClose to set open: false in your state. The sheet doesn't manage its own closed state.aria-labelledby pointing to a heading inside the card. The sheet card supports forwarding standard div props.open. The component returns null when closed but it's clearer to gate at the call site.Recipes
This component appears in:
- API recipe — endpoint detail surfaces.
- Docs corpus recipe — confirmation flows for delete-document actions.
Code example
import { Sheet, Card } from "@matter/components";
import { useState } from "react";
export function ConfirmDelete({ onConfirm }: { onConfirm: () => void }) {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)} type="button">
Delete entity
</button>
<Sheet open={open} onClose={() => setOpen(false)} aria-labelledby="confirm-h">
<h2 id="confirm-h">Delete this entity?</h2>
<p>This dissolves the entity and revokes every active token.</p>
<div className="flex gap-2 justify-end mt-4">
<button onClick={() => setOpen(false)} type="button">Cancel</button>
<button onClick={onConfirm} type="button">Delete</button>
</div>
</Sheet>
</>
);
}Source
packages/components/src/SheetCard
Opaque elevated surface — bg-elev + hairline border + radius-lg. The default container for content blocks anywhere in the dashboard or marketing surfaces. Pass `padding` to override the 32px default.
Glass
Translucent liquid-glass surface — backdrop blur and saturation, top specular highlight, bottom edge shadow, soft radial light overlay. Place over a bloom panel or any saturated background to see the refraction land. `tone` switches the light/dark recipe.