Skip to content

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

Do — render Sheet for a confirmation flow ("Discard changes?") or a focused detail view (the endpoint detail under the API explorer).
Don't — render Sheet for a multi-step wizard. The lack of focus trap will catch keyboard users by surprise; use Dialog instead.

Anatomy

Three structural pieces, portaled to document.body:

  1. Scrim. Full-viewport overlay. role="dialog", aria-modal="true". Backdrop click triggers onClose.
  2. Card. Centered floating panel. Composes .sheet-card.glass — the Glass surface with a top specular highlight and bottom edge shadow. tint prop adjusts the background tint via --sheet-card-tint. width defaults to min(560px, 100%).
  3. Close button. Top-right × button. Rendered when showClose is true (default). Always carries aria-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

PropTypeDefaultDescription
openrequiredboolean
onClose() => void
tintstring"20, 17, 13"
widthnumber | string"min(560px, 100%)"
showClosebooleantrue
childrenReact.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

StateTriggerBehaviour
Closed (default)open: falseReturns null; nothing in the DOM
Openopen: trueScrim and card mount via portal; Escape listener binds
ClosingonClose firesConsumer 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

No tokens match.
colors1 tokensv2.3.0
rgba(229, 229, 229, 0.64)
brand
src ↗
radii15 tokensv2.3.0
10
matter
src ↗
16
matter
src ↗
24
matter
src ↗
32
matter
src ↗
72
matter
src ↗
999
matter
src ↗
6
brand
src ↗
10
brand
src ↗
12
brand
src ↗
16
brand
src ↗
20
brand
src ↗
24
brand
src ↗
32
brand
src ↗
72
brand
src ↗
9999
brand
src ↗

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/headless for the trap-enabled version.
  • ARIA roles and properties. role="dialog" + aria-modal="true". Consumer must supply aria-labelledby pointing 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-labelledby wired, 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 → Canvas with ButtonBorder. Close button → ButtonText on ButtonFace.
  • WIG rules. overscroll-behavior: contain on 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

Do — wire onClose to set open: false in your state. The sheet doesn't manage its own closed state.
Don't — render Sheet with focus-trap-required content (multi-step forms with keyboard navigation). Use Dialog from /headless.
Do — supply aria-labelledby pointing to a heading inside the card. The sheet card supports forwarding standard div props.
Don't — render long-form content inside Sheet. If the user needs to read more than a screen height, route to a page instead.
Do — render the sheet conditionally based on open. The component returns null when closed but it's clearer to gate at the call site.
Don't — open multiple Sheets simultaneously. There's no escape-stack management — the global Escape listener would close all of them.

Recipes

This component appears in:

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/Sheet

On this page