Sheet
Slides in a panel to edit one thing in place.
Examples
Side
enddefault
start
bottom
top
Usage
When to use it
- Use
- Editing one item from a list the person wants in mind: an agent, an integration.
- Use
- Showing the details of the selected row next to its list (docked).
- Use
- Navigation or a few actions on a phone, from the left or the bottom.
- Not for
- A yes or no question, or a form of up to three fields. Use Dialog.
- Not for
- Content of a few lines next to its control. Use Popover.
- Not for
- The panel is the main subject of the page. Use a route.
Rules
12 rules
- Must
- Follow the dialog shell: grey outer with a quiet frame line, one white card 4px in that holds the title, description and close and goes on as the body, and the actions on the grey. Because the grey gives the actions a ground of their own, and the white card is the one group that carries the heading and the rows, so the frame never needs a rule under its header.
- Never
- Put a white Card inside a sheet; the inset is the only white surface inside it. Because a white card inside the sheet's grey is a second box in the box.
- Must
- Title the sheet with the item's name or ID and put context in the description. Because the title is the sheet's accessible name, so "Details" tells a screen reader nothing about which ticket is open.
- Must
- Put the middle in SheetBody; it is the only part that scrolls and fades. Because SheetBody is the only part that scrolls and fades, so without it the footer scrolls off with the fields.
- Should
- Use modal={false} with stackBelow for the details of a selected row; a modal sheet hides the list.
- Should
- Store the width from onWidthChange per surface and pass it back as defaultWidth on a resizable docked panel.
- Must
- Give actions in a side="bottom" sheet size="lg" (44px). Because a bottom sheet is the phone layout, and a 28px sm button is under the 44px thumb target.
- Should
- Pass mono to SheetField for an ID, hash or URL.
- Must
- Open a docked panel with SheetTrigger, or close it with SheetClose, so Escape can return focus. Because the docked panel returns focus to the element SheetTrigger registered, so a plain button leaves focus lost on Escape.
- Never
- Add border-l to a docked panel; it is already a frame in the layout. Because the docked panel draws its own frame line, and a border on top doubles the edge.
- Never
- Wrap the body in a scroll library; SheetBody has native overflow and a fade. Because a second scroller inside SheetBody hides the edge fade and gives the keyboard two scroll areas.
- Should
- Keep minWidth at 280px or more and stack under 3xl of the container.
Accessibility
7 checks
- A modal panel is role="dialog" with aria-modal, named by SheetTitle; the page behind is inert and Tab stays inside.
- A docked panel is an aside named by SheetTitle; it does not trap focus.
- Escape closes it, a menu or popover inside closes first, and nothing closes while an action is pending.
- Focus returns to the trigger on close.
- The docked handle is role="separator" with aria-valuenow, min, max; arrows move 32px, Shift 8px, Home and End jump, Enter resets.
- The body is a tab stop only while it has more to scroll.
- A rejected onAction shows a role="alert" message above the footer.
Edge cases
11 edge cases
- 60 fields
- The body scrolls inside; header and footer stay; the fade shows only at an edge with more.
- Slow save
- The action shows pending, Escape and Close wait, and the sheet stays open.
- Failed save
- The sheet stays open with the message above the footer; fields keep their input.
- Right to left
- side="end" comes from the left and swipes back to it; a docked panel takes the wrapper dir, its handle on the edge facing the list.
- Long trace ID
- A mono field breaks anywhere and wraps.
- Long title
- Wraps in the modal sheet; truncates to one line in a docked one.
- No title
- The panel is unnamed. Always render SheetTitle.
- Zoom 200%
- A docked panel stacks under the list; a modal body scrolls.
- Menu inside
- Escape closes the menu first; a docked panel ignores Escape from an open menu or dialog.
- Forced colors
- The panel gets a 1px CanvasText outline; a docked frame's line survives as CanvasText.
- Reduced motion
- Fades instead of travelling its 8px; a swipe still follows the finger.
API
11 props
Sheet modalbooleandefaulttrueSheet open / defaultOpen / onOpenChangeboolean, (open) => voiddefaultfalseSheetContent side'start' | 'end' | 'top' | 'right' | 'bottom' | 'left'default'end'SheetContent showCloseButtonbooleandefaulttrueSheetContent kindKindSheetContent defaultWidth / minWidth / maxWidthnumberdefault360 / 280 / 640SheetContent onWidthChange(width) => voidSheetContent stackBelow'md' to '5xl'SheetFooter defaultAction'primary' | 'dismiss' | 'none'defaultautoSheetAction onAction() => void | PromiseSheetField monobooleandefaultfalse
Tokens
27 tokens
--surface-overlay- oklch(var(--ink-c) / 0.24)
--surface-page- var(--paper)
--elev-overlay- 0 0 0 1px var(--line-frame), 0 12px 24px -12px oklch(var(--text-on-field-c) / var(--shadow-alpha))
--overlay- 480px
--space-10- 40px
--radius-soft- 6px
--radius-frame- 10px
--line-frame- oklch(var(--ink-c) / var(--line-frame-alpha))
--line-row- oklch(var(--ink-c) / 0.08)
--line-control- oklch(var(--ink-c) / 0.16)
--accent-mark- var(--ink)
--dur-slow- 180ms
--dur-base- 160ms
--sheet-shift- 8px
--ease-step- cubic-bezier(0.2, 0, 0, 1)
--ink- oklch(var(--ink-c))
--dur-fast- 120ms
--surface-inverse- var(--ink)
--text-inverse- var(--paper)
--type-state-word- 400 14px/16px var(--font-sans)
--accent-wash- var(--surface-selected)
--text-1- var(--ink)
--text-3- oklch(var(--ink-c) / 0.66)
--type-label- 400 14px/16px var(--font-sans)
--type-code-sm- 400 12px/16px var(--font-mono)
--type-body-sm- 380 13px/18px var(--font-sans)
--grab-size- 40px