Sheet
A dialog pinned to an edge of the viewport, for secondary work that keeps the page in view: settings, details, mobile navigation. Same focus trap and Esc behaviour as Dialog. Source: src/components/ui/sheet.tsx.
Sides
Four edges. Right is the default for detail and settings panels; left is navigation (the docs' own mobile menu); bottom is a phone action sheet; top is rare.
Form panel
Header and footer stay pinned while the body scrolls. Hairlines mark the two edges. Save submits the form and closes the sheet.
Without the corner close
showCloseButton={false} drops the X when the footer already has a clear way out, and overlayClassName adjusts the scrim for one sheet without touching the rest.
Motion and layering
The slide and fade are CSS keyframes (tw-animate), so a sheet closed mid-slide does not reverse smoothly, and there is no drag-to-dismiss. Under reduced motion the global rule cuts both to near-zero.
| Value | Token | Usage |
|---|---|---|
| 50 | Scrim and panel. The brand ladder (--z-modal 200) is defined but not wired in yet. | |
| 220ms | Enter: the panel slides 40px (slide-in-from-*-10) and fades in. | |
| 90ms | Exit: faster than the entrance. | |
| quint-out | The panel's curve, for a larger move than a popover's. | |
| 200ms | The scrim's fade, shared with Dialog. | |
| 3/4 · 24rem | Left and right sheets: 75% of a phone, capped at 384px from sm. |
Props
| Part / prop | Type | Default | Description |
|---|---|---|---|
| Sheet | Radix Dialog.Root | — | open, defaultOpen, onOpenChange, modal. |
| SheetTrigger | Radix Dialog.Trigger | — | Use asChild around a Button. Focus returns here on close. |
| SheetContent | Radix Dialog.Content | — | Portals itself with its own scrim. A flex column with a 16px gap. |
| side | "top" | "right" | "bottom" | "left" | "right" | Which edge it is pinned to. Mirrored as data-side. |
| showCloseButton | boolean | true | A ghost icon-sm X at top-right with a sr-only “Close”. |
| overlayClassName | string | — | Merged onto the scrim, e.g. backdrop-blur-none for one sheet. |
| SheetHeader | ComponentProps<"div"> | — | 16px padded stack for title and description. |
| SheetTitle | Radix Dialog.Title | — | Required for the accessible name. 18px heading face. |
| SheetDescription | Radix Dialog.Description | — | Wired to aria-describedby. |
| SheetFooter | ComponentProps<"div"> | — | Pushed to the bottom (mt-auto), stacked. Add sm:flex-row for a button row. |
| SheetClose | Radix Dialog.Close | — | Any element that closes the sheet. Use asChild around a Button. |
Best practices
When to use
- A sheet keeps context: the user can see what they are editing behind it. Use a Dialog when the page should step back.
- Navigation on a phone is a left sheet. Long detail views and filters are a right sheet.
- Don't stack a sheet on a sheet. Navigate inside it, or close it and open a dialog.
Behavior
- Esc, a scrim click, the X and any SheetClose all close it; focus returns to the trigger.
- Pin the header and footer and scroll the body, so the primary action is always reachable.
- Bottom sheets keep the safe-area in mind: put the primary action last and full width.
Accessibility
- Every sheet needs a SheetTitle; sr-only it if the design hides it.
- The X is labelled “Close” for screen readers. A custom close button needs the same.
- Focus is trapped inside while open; tab order follows the DOM, so keep the footer last.
Content
- Title the sheet after the object or task (“Edit schedule”, “Run 4182”).
- Keep sheets to one task. If it needs tabs and a wizard, it wants its own page.