Component · Containers & Overlays
Sheet
Overview
An edge-attached overlay panel — Dialog positioned flush against a screen edge instead of centered. Sheet inherits Dialog's modal behavior contract (see Focus trap and restoration) and adds edge placement and mobile-specific considerations.
Anatomy
Overlay (backdrop) → Panel → Header (Title + Description + Close control) → Body → optional Footer (actions).
- Overlay — the scrim behind the panel.
- Panel — the surface attached to one edge.
- Header — Title, Description, and Close control.
- Body — the main content.
- Footer — optional action row.
Same part list as Dialog's Title/Description/Body/Actions/Close, arranged in a panel attached to one edge instead of a centered floating surface.
Live Example
Right (default) — a filters panel
Bottom — a mobile-style sheet
Right, unsaved changes — overlay-click disabled
Code Example
"use client";
import { X } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Sheet,
SheetBody,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
/**
* Live Sheet example — a Sheet is a Dialog attached to a screen edge, so the
* focus trap, `Escape` / overlay-click dismissal, focus restoration, and
* action ordering all behave exactly as Dialog's do.
*
* - Right (default): a desktop-style secondary panel, full height, capped
* width. Rounded on its left corners only; the right edge is flush.
* - Bottom: a mobile-style bottom sheet, full width, capped height (never the
* full viewport) — its body scrolls. No drag handle: this component doesn't
* implement swipe-to-dismiss, so it doesn't show an affordance for one.
* - Right, unsaved changes: `dismissible={false}`, so an accidental overlay
* click can't discard the edits — Cancel and `Escape` remain.
*/
export function SheetShowcase() {
return (
<div className="flex flex-col gap-8">
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Right (default) — a filters panel
</h3>
<Sheet>
<SheetTrigger>
<Button variant="outline">Open filters</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Filters</SheetTitle>
<SheetDescription>
Narrow the list without leaving the page behind it.
</SheetDescription>
<SheetClose>
<Button
variant="ghost"
size="icon"
aria-label="Close"
className="absolute right-4 top-4"
>
<X />
</Button>
</SheetClose>
</SheetHeader>
<SheetBody>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="filter-owner">Owner</Label>
<Input id="filter-owner" placeholder="Anyone" />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="filter-label">Label</Label>
<Input id="filter-label" placeholder="Any label" />
</div>
</div>
</SheetBody>
<SheetFooter>
<SheetClose>
<Button variant="ghost">Reset</Button>
</SheetClose>
<SheetClose>
<Button>Apply</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Bottom — a mobile-style sheet
</h3>
<Sheet>
<SheetTrigger>
<Button variant="outline">Choose a plan</Button>
</SheetTrigger>
<SheetContent side="bottom">
<SheetHeader>
<SheetTitle>Choose a plan</SheetTitle>
<SheetDescription>
The sheet is capped below the full viewport height; this list
scrolls inside it.
</SheetDescription>
</SheetHeader>
<SheetBody>
<ul className="flex flex-col gap-3">
{Array.from({ length: 10 }, (_, i) => (
<li
key={i}
className="rounded-md border-[length:var(--scale-px)] border-solid border-border p-3"
>
Plan tier {i + 1} — a short description of what this tier
includes and who it suits.
</li>
))}
</ul>
</SheetBody>
<SheetFooter>
<SheetClose>
<Button variant="ghost">Not now</Button>
</SheetClose>
<SheetClose>
<Button>Continue</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Right, unsaved changes — overlay-click disabled
</h3>
<Sheet dismissible={false}>
<SheetTrigger>
<Button variant="outline">Edit profile</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>
Changes here aren’t saved until you choose Save.
</SheetDescription>
</SheetHeader>
<SheetBody>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor="profile-name">Display name</Label>
<Input id="profile-name" defaultValue="Ada Lovelace" />
</div>
<div className="flex flex-col gap-2">
<Label htmlFor="profile-title">Title</Label>
<Input id="profile-title" defaultValue="Mathematician" />
</div>
</div>
</SheetBody>
<SheetFooter>
<SheetClose>
<Button variant="ghost">Cancel</Button>
</SheetClose>
<SheetClose>
<Button>Save</Button>
</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
</section>
</div>
);
}Variants
Supported placement — Version 1
| Placement | Attaches to | Width/height | Typical use |
|---|---|---|---|
| Right (default) | Right edge, full viewport height | Fixed width, content-appropriate (e.g. capped near --breakpoint-sm) | Desktop-style secondary panel — filters, details, a focused form |
| Bottom | Bottom edge, full viewport width | Capped max-height (not full viewport) | Mobile-style bottom sheet |
Left and top placements are not supported for Version 1 — no current use case needs them, and adding placements without a real need would be speculative.
A side Sheet and a bottom Sheet are not the same component skinned two ways — they serve different contexts (desktop secondary panel vs. mobile-native pattern) and, as documented below, support different dismissal affordances. Don't conflate them.
Rounded corners by placement
Only the corners facing away from the flush edge are rounded, at --radius-xl (matching
Dialog's "Modals, large surfaces" token):
| Placement | Rounded corners |
|---|---|
| Right | Top-left and bottom-left only — the right edge is flush with the viewport |
| Bottom | Top-left and top-right only — the bottom edge is flush with the viewport |
States
| State | Look |
|---|---|
| Closed | Not rendered |
| Open | Overlay visible, panel --background fill, edge-appropriate --radius-xl corners, --shadow-lg |
Usage Guidance
Relationship to Dialog
Sheet is a Dialog, placed at an edge. Everything Dialog documents for overlay
behavior, focus trap, restoration, Escape/close handling, and destructive-action ordering applies
to Sheet unchanged — this page documents only what differs: anatomy shape, placement, and the
mobile bottom-sheet case. Use Sheet instead of a centered Dialog when retaining more of the
underlying page's visual context at the edge helps, or for a mobile-style bottom-anchored flow;
don't use Sheet as a universal replacement for pages — the same
full-page guidance Dialog documents applies here too.
Modal vs. non-modal
Sheet is modal by default, inheriting Dialog's full contract: overlay present, background inert, focus trapped inside the panel while open, focus restored to the trigger on close. A non-modal edge panel is a different pattern (closer to a wide Popover) and isn't what "Sheet" means in this system.
Sheet also inherits Dialog's conditional overlay-click dismissal unchanged: standard, low-risk Sheets may allow overlay-click dismissal; a Sheet used for a destructive confirmation, an unsaved-change flow, or a critical acknowledgement disables it, the same as Dialog.
Drag handle and swipe gesture
Not assumed by default. A drag handle (a small horizontal pill at the top of the panel) is a common mobile convention for the Bottom placement specifically, and may be added only when the implementation genuinely supports swipe-to-dismiss — the handle is a visual affordance for a real gesture, not decoration. Without swipe support, don't render a handle; it would promise an interaction that doesn't exist.
The Right placement never gets a drag handle — dragging a desktop side panel isn't an
established interaction, and dismissal there is the same as Dialog: close control, Escape, or
overlay click.
Long content and internal scrolling
Same as Dialog: Header and Footer stay fixed; only the Body scrolls internally once content exceeds the panel's available height. The Bottom placement's capped max-height (rather than full viewport height) makes this more likely to apply than on a full-height Right panel.
Tokens
Sheet reuses Dialog's token set with edge-specific radius application; it introduces no component-specific styling values.
| Token | Where used |
|---|---|
--background / --foreground | Panel fill + text |
--muted-foreground | Description text |
--border | Panel edge, useful on dark mode |
--radius-xl | Corner radius, applied only to the two non-flush corners |
--shadow-lg | Elevation — Shadows names "Modals, dialogs, sheets" |
--ring | Focus ring on interactive content and the close control |
--scale-6 | Panel padding |
--scale-4 | Gap between Header/Body/Footer |
The overlay scrim reuses Dialog's explicit rgb(0 0 0 / 48%) value unchanged — see
Dialog: Overlay scrim; it isn't a token.
Do / Don’t
Do
- Treat Sheet as Dialog's edge-attached form — reuse its focus, dismissal, and action-ordering rules rather than reinventing them.
- Cap the Bottom placement's height — never let it consume the full viewport height by default.
- Only add a drag handle on the Bottom placement, and only when swipe-to-dismiss is actually wired up.
Don’t
- Add Left or Top placements without a real, repeated Version 1 need.
- Give a Right-placed Sheet a drag handle or swipe affordance.
- Use Sheet as a general page-replacement pattern — see Dialog's full-page guidance.
Accessibility
Inherits Dialog's accessibility contract in full: role="dialog" with
aria-modal="true", aria-labelledby/aria-describedby referencing Title/Description, focus trap
and restoration, and background inertness while open. No additional roles are introduced by edge
placement — placement is a visual/layout decision, not a semantic one.
Responsive Behavior
The Bottom placement is the mobile-appropriate pattern for Sheet. In Version 1 a Right-placed Sheet
does not convert itself to a Bottom presentation on a narrow viewport: it is capped at
--breakpoint-sm wide, so below that it simply becomes full-width. Choosing Bottom for a
mobile-first surface is the caller's decision, in line with the caution Dialog records about not silently merging distinct
responsive patterns.