Sheet
A panel that slides in from a screen edge — Dialog semantics with a side, a header and a footer, and a close button in the corner.
- Status
- Since
0.1.0- Accessibility pattern
- APG dialog (modal)
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Sheet before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify sheetInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/sheetRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/button, @vegastack/use-modal-inert.
Usage
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
<Sheet>
<SheetTrigger>Open</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Are you absolutely sure?</SheetTitle>
<SheetDescription>This action cannot be undone.</SheetDescription>
</SheetHeader>
</SheetContent>
</Sheet>;Anatomy
Examples
Composition
A Sheet is Base UI's Dialog with edge-anchored positioning, so the tree is the Dialog tree.
SheetContent is a flex column and SheetFooter carries mt-auto, which is what pins the footer
to the bottom of the panel however little content sits above it. The footer is as plain as the
header and its actions are right-aligned at every width, primary last; secondary actions use
variant="secondary". To seat a Cancel at the start edge instead, give it
data-slot="sheet-cancel" — SheetFooter pushes that child left with me-auto (OVL-18):
<SheetFooter>
<SheetClose data-slot="sheet-cancel" render={<Button variant="secondary" />}>
Cancel
</SheetClose>
<Button>Save</Button>
</SheetFooter>Sheet
├── SheetTrigger
└── SheetContent
├── SheetHeader
│ ├── SheetTitle
│ └── SheetDescription
└── SheetFooterSide
side on SheetContent — top, right (the default), bottom or left — picks the edge. It is
published as data-side and every inset, border, width cap and slide offset is a
data-[side=…] branch on that one attribute, so there is no second class list to keep in step. The
left and right panels cap at sm:max-w-sm; top and bottom size to their content unless you cap
them.
No Close Button
showCloseButton={false} on SheetContent removes the corner close control. Nothing else replaces
it, so give the panel its own way out — a SheetClose in the footer.
RTL
The panel's borders and its close button are written in logical properties, and each slide offset
carries an explicit rtl: counterpart, so a right-to-left document mirrors the motion as well as
the layout. Mirror the anchor too: a panel that opens from right in a left-to-right document opens
from left in a right-to-left one. And hand dir to SheetContent — the panel portals to
<body>, so a dir on an ancestor of the trigger never reaches it.
Sizes
size on SheetContent steps a left or right sheet through Dialog's own width scale (OVL-16), so
size="lg" is the same cap on either overlay: sm (max-w-xs), default (max-w-sm, upstream's
width), lg (max-w-2xl) and xl (max-w-5xl), from the sm breakpoint up. The value is
published as data-size. Below sm a side sheet is full width whatever its size, and from sm up
it is three quarters of the viewport under that cap, so a quarter of the page always shows beside
it and xl reaches its full 64rem only from a 1366px viewport (960px at 1280px). Top and bottom
sheets ignore the axis.
Scrolling body
Put long content in SheetBody. It is the one scroller in the panel's flex column
(min-h-0 flex-1 overflow-y-auto), so the header stays at the top and the footer's actions stay on
screen however many fields sit between them. It carries the panel's px-4 inset itself.
A top or bottom sheet is as tall as its content up to 85dvh, then stops: a long thread on a
phone scrolls inside SheetBody rather than running past the screen, and the footer stays in view.
Header action
SheetAction seats one control at the end of SheetHeader, beside the title and description, and
reserves the corner close button's width so the two never meet. closeLabel on SheetContent
renames that close button — here "Close invoice" instead of the default "Close". Without an action the
header keeps clear of the close button itself, so a long title wraps before the × (LAY-17).
Beside the sidebar
beside="sidebar" docks a side="left" sheet at the sidebar rail's edge (--sidebar-width), and
beside="sidebar-icon" at the collapsed rail (--sidebar-width-icon), so a panel such as the
Inbox slides out beside the navigation without a hard-coded offset. Pair it with
showOverlay={false} for a non-modal docked panel.
<SheetContent
side="left"
beside={collapsed ? "sidebar-icon" : "sidebar"}
showOverlay={false}
>
…
</SheetContent>Opening any sheet puts focus on its first field or control — never the close × — or on the panel itself.
Playground
Pick the edge the panel pins to and whether it carries a close button, then copy the generated JSX.
<Sheet>
<SheetTrigger render={<Button variant="outline">Open sheet</Button>} />
<SheetContent>
<SheetHeader>
<SheetTitle>Edit profile</SheetTitle>
<SheetDescription>Make changes to your profile here.</SheetDescription>
</SheetHeader>
<SheetFooter>
<SheetClose render={<Button variant="secondary">Cancel</Button>} />
<Button>Save changes</Button>
</SheetFooter>
</SheetContent>
</Sheet>Sections
A sheet's body is usually a few sections. SheetSection holds a SheetSectionHeader — a
SheetSectionTitle (14px medium) with an action pushed to its end — over its content, 8px apart;
SheetSectionDescription is a section's muted line (its state, an empty note) and
SheetSectionLabel a quiet 12px label over a run of cards ("Earlier"). Space the sections with a
gap on SheetBody.
<SheetBody className="flex flex-col gap-6">
<SheetSection>
<SheetSectionHeader>
<SheetSectionTitle>Edits since you verified</SheetSectionTitle>
<Button variant="ghost" size="xs">
Compare
</Button>
</SheetSectionHeader>
<SheetSectionDescription>No edits since.</SheetSectionDescription>
</SheetSection>
</SheetBody>API Reference
Sheet adds no props of its own — it accepts everything the underlying element or Base UI primitive accepts (className, ref, ARIA attributes, event handlers).
| Prop | Type | Default | Description |
|---|---|---|---|
beside | "sidebar" | "sidebar-icon" | — | Dock a side="left" sheet beside the app's sidebar rail instead of over it: "sidebar" starts
it at --sidebar-width, "sidebar-icon" at the collapsed --sidebar-width-icon. |
closeLabel | string | — | |
showCloseButton | boolean | — | |
showOverlay | boolean | — | false drops the dimmed backdrop — for a non-modal panel docked beside the page. |
side | "bottom" | "left" | "right" | "top" | — | |
size | "default" | "lg" | "sm" | "xl" | — |
SheetContent adds size ("sm" | "default" | "lg" | "xl", default "default"; side sheets
only), closeLabel (the corner close button's name, default "Close") and showOverlay
(default true; false drops the dimmed backdrop for a non-modal panel docked beside the page).
SheetTrigger, SheetClose, SheetHeader, SheetAction, SheetBody, SheetFooter,
SheetTitle and SheetDescription add no props of their own — each accepts everything its Base UI part or its div accepts. The portal
and the backdrop are internal to SheetContent and are not exported.
Accessibility
- Renders Base UI's Dialog: the panel is
role="dialog"witharia-modal, named bySheetTitlethrougharia-labelledbyand described bySheetDescriptionthrougharia-describedby. Esc closes, focus is trapped in the panel and returned to the trigger. - Give every sheet a
SheetTitle. Without one the panel has no accessible name; if it must not be seen, keep it and addclassName="sr-only". - The background is made natively inert while the panel is open (A11Y-9). With
modal={false}on the root nothing is made inert, so the page behind stays interactive — that is the mode's point. - The panel portals to
<body>, soSheetPortalre-applies the enclosing theme scope on adisplay: contentshost inside the portal (OVL-13). - The corner close button is an icon-only
Buttoncarrying asr-onlylabel, "Close" unlesscloseLabelnames it.
| Contract | States tested |
|---|---|
| Behaviour | default, open, closed, modal, non-modal |
| Accessibility | native-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation |
| Visual | default, open, closed, hover, focus |
Do / Don't
Deviations
Upstream's file plus packages/ui/upstream/patches/sheet.patch. Every hunk:
- API-29 —
SheetContenttakesbeside="sidebar" | "sidebar-icon": a left sheet starts atvar(--sidebar-width)/var(--sidebar-width-icon)(written asdata-beside). - FOC-13 —
SheetContentpasses an overridableinitialFocusthat picks the first field or control, never the close ×, else the panel itself. - OVL-13 —
SheetPortalreadsuseInternalThemeScope()and wraps its children in adisplay: contentselement carrying that class. - A11Y-9 —
SheetContentmirrors Base UI'sdata-base-ui-inertmarkers onto nativeinertthroughuseModalInert, gated on the root'smodalprop published through a context — the same pairdialogcarries, because upstream's Sheet is Base UI's Dialog. - OVL-16 —
SheetContenttakessize?: "sm" | "default" | "lg" | "xl"and reflects it asdata-size; a left or right sheet steps Dialog's scale (sm:max-w-xs·sm:max-w-sm·sm:max-w-2xl·sm:max-w-5xl), top and bottom ignore it, and belowsma side sheet is full width. Widened from Dialog by MK 2026-09-24. - API-21 —
SheetBodyscrolls between the header and the footer, and a top or bottom sheet stops atmax-h-[85dvh]so it can;SheetActionis an end seat inSheetHeaderthat clears the close button;closeLabelrenames the corner close control. - OVL-18 —
SheetFooteris a right-aligned wrapping row (flex flex-wrap items-center justify-end) at every width, not upstream's full-widthflex-colstack; a direct child withdata-slot="sheet-cancel"takesme-autoto sit at the start edge. MK, 2026-09-25. - VOI-1 — the corner close's built-in string is the overridable
closeLabel, default "Close". - LAY-17 —
SheetContentsetswrap-break-word, so a long unbroken word breaks inside the panel rather than running past its edge. While the corner × renders, aSheetHeaderwith noSheetActiontakespe-12, so a long title wraps before the × instead of running under it. With aSheetActionthe header's text track isminmax(0, 1fr), so a long title wraps instead of pushing the action off the panel. MK, 2026-10-13. - LAY-10 — a bottom sheet pads its panel by
env(safe-area-inset-bottom), so its footer and last row clear a phone's home indicator (zero where there is none). - DOC-2 —
import { cn } from "cn"becomes@vegastack/design; our npm layer ownscn. - DOC-1 — prettier reflow plus the
registry-stamp.mjsprovenance header; no decision of their own. - OVL-22 — below the
tablettier (40rem) a side sheet is the whole screen: full width, no side border, and padded by the top and bottom safe-area insets. MK, 2026-10-26.