Sheet stackPro

Nested sheets that stack with depth, drag to dismiss, and become stacked dialogs on wide screens.

Live · keyboard ready
  • Mobile settings where each row opens a deeper page and back should feel spatial.
  • Nested pickers, such as choosing a workspace and then a project inside it.
  • Flows that should be a bottom sheet on phones and a centered dialog on desktop from one declaration.
  • Use dialog for a single confirmation or form with no nested steps.
  • Use bottom-sheet for one sheet with snap points.
  • Use multi-step-form when steps are sequential rather than drill-down.

Installation

Pro source and install commands unlock with a Pro plan.

example.tsx
import { Sheet, SheetStack, SheetTrigger, useSheetStack } from "@/registry/components/sheet-stack/sheet-stack"; function SaveButton() {  const { close } = useSheetStack();  return <button type="button" onClick={close}>Save</button>;} export function Settings() {  return (    <SheetStack>      <SheetTrigger sheet="settings">Settings</SheetTrigger>       <Sheet id="settings" title="Settings">        <SheetTrigger sheet="notifications">Notifications</SheetTrigger>      </Sheet>       <Sheet id="notifications" title="Notifications" footer={<SaveButton />}>        <label><input type="checkbox" defaultChecked /> Email me about mentions</label>      </Sheet>    </SheetStack>  );}

API reference

4 parts. The first is the root.

SheetStack

Holds the stack of open sheets and renders the layer they appear in. Declare every Sheet inside one SheetStack.

PropTypeDefaultDescription
childrenRequiredReactNode–Page content, triggers, and Sheet declarations.
stackstring[]–Controlled ids of open sheets, bottom first. Leave undefined for uncontrolled use.
defaultStackstring[][]Sheets open at first when uncontrolled.
onStackChange(stack: string[]) => void–Called when sheets are pushed or popped.
mode"auto" | "sheet" | "dialog""auto"auto picks bottom sheets below breakpoint and centered dialogs above it.
breakpointnumber640Width in px of the viewport, or the container when contained, where auto switches to dialogs.
containedbooleanfalseFill the nearest positioned ancestor instead of the viewport.

Sheet

One level of the stack. Renders nothing until its id is pushed.

PropTypeDefaultDescription
idRequiredstring–Id used by push, popTo, SheetTrigger, and the stack array.
titleRequiredstring–Heading and accessible name.
descriptionstring–A line under the title, wired as the dialog description.
childrenRequiredReactNode–Scrolling body content.
footerReactNode–A row pinned under the scrolling body, such as the primary action.
backLabelstring–Label for the back button of a sheet opened from this one. Defaults to title.
dismissiblebooleantrueAllow drag and fling to dismiss.
classNamestring–Class on the panel.
refRef<HTMLDivElement>–The panel element.

SheetTrigger

A button that opens a sheet on top of the stack. Takes every button attribute.

PropTypeDefaultDescription
sheetRequiredstring–Id of the sheet to push.
refRef<HTMLButtonElement>–The button element.
...propsButtonHTMLAttributes<HTMLButtonElement>–Forwarded to the button. Call preventDefault in onClick to skip the push.

useSheetStack

Returns { stack, push, pop, popTo, close } for buttons inside or beside the sheets. Throws outside SheetStack.

No props.

Escape
Closes the top sheet only.
TaborShift+Tab
Cycles focus inside the top sheet.
EnterorSpace
Runs back, close, and trigger buttons.
  • Each panel is a dialog labelled by its title and described by description; only the top one is aria-modal, lower ones are inert.
  • Opening moves focus into the new sheet; popping returns focus to the element that opened it.
  • The back button names the sheet it returns to; the close button reads "Close all" when more than one sheet is open.
  • Page scroll is locked while any sheet is open, unless contained.
  • A new sheet rises from the bottom edge (or eases in as a dialog) while the one below scales back 5%, dims, and peeks above it.
  • The top sheet follows a vertical drag with rubber banding upward; the sheets below ease back up as it goes and a fling past 35% or 450px/s pops it.
  • Height changes spring as content changes, and all springs keep velocity when interrupted.
  • Reduced motion removes the depth scale, lift, and drag physics; sheets fade in and out.
  • mode="auto" measures the layer with a ResizeObserver: under breakpoint (640px) sheets rise from the bottom; at or above it they are centered dialogs.
  • Each level loses room for the peek (10px per level for sheets, 14px for dialogs), and bodies scroll inside past that height.
  • Drag to dismiss works with any pointer; a drag starting in a scrolled body scrolls instead of dragging.
  • Transforms and dims come from motion values, so drags and depth changes do not rerender React per frame.
  • Each open sheet has a ResizeObserver on its content; the stack is meant for a handful of levels, not dozens.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Use for short drill-down flows (settings, filters, account steps) where each level should keep its parent in view.
  • Declare every Sheet once inside the SheetStack; open with SheetTrigger or useSheetStack().push. Pushing an open id pops back to it.
  • Add data-sheet-no-drag to content that handles its own vertical drags, such as sliders or maps.
  • For a single sheet with no nesting use bottom-sheet or drawer.

The full library index for assistants is at /llms.txt.