Skip to content
Component installs need the registry setup
VegaStack Design

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
stable
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 sheet

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/sheet

Run 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

Sheet — data-slot="sheet"
SheetTrigger — data-slot="sheet-trigger"
SheetClose — data-slot="sheet-close"
SheetContent — data-slot="sheet-close" | "sheet-content"
SheetHeader — data-slot="sheet-header"
SheetAction — data-slot="sheet-action"
SheetBody — data-slot="sheet-body"
SheetFooter — data-slot="sheet-footer"
SheetTitle — data-slot="sheet-title"
SheetDescription — data-slot="sheet-description"
SheetSection — data-slot="sheet-section"
SheetSectionDescription — data-slot="sheet-section-description"
SheetSectionHeader — data-slot="sheet-section-header"
SheetSectionLabel — data-slot="sheet-section-label"
SheetSectionTitle — data-slot="sheet-section-title"

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
    └── SheetFooter

Side

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).

PropTypeDefaultDescription
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.
closeLabelstring—
showCloseButtonboolean—
showOverlayboolean—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" with aria-modal, named by SheetTitle through aria-labelledby and described by SheetDescription through aria-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 add className="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>, so SheetPortal re-applies the enclosing theme scope on a display: contents host inside the portal (OVL-13).
  • The corner close button is an icon-only Button carrying a sr-only label, "Close" unless closeLabel names it.
ContractStates tested
Behaviourdefault, open, closed, modal, non-modal
Accessibilitynative-or-base-ui-semantics, browser-accessibility-test, keyboard-navigation
Visualdefault, open, closed, hover, focus

Do / Don't

Do
Use a Sheet for a side task with room to breathe — filters, a detail panel, a form — anchored to the edge it relates to.
Don't
Use a Sheet where a Dialog fits, or open one from inside another; a panel that covers three quarters of the screen is not a popover.

Deviations

Upstream's file plus packages/ui/upstream/patches/sheet.patch. Every hunk:

  • API-29 — SheetContent takes beside="sidebar" | "sidebar-icon": a left sheet starts at var(--sidebar-width) / var(--sidebar-width-icon) (written as data-beside).
  • FOC-13 — SheetContent passes an overridable initialFocus that picks the first field or control, never the close ×, else the panel itself.
  • OVL-13 — SheetPortal reads useInternalThemeScope() and wraps its children in a display: contents element carrying that class.
  • A11Y-9 — SheetContent mirrors Base UI's data-base-ui-inert markers onto native inert through useModalInert, gated on the root's modal prop published through a context — the same pair dialog carries, because upstream's Sheet is Base UI's Dialog.
  • OVL-16 — SheetContent takes size?: "sm" | "default" | "lg" | "xl" and reflects it as data-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 below sm a side sheet is full width. Widened from Dialog by MK 2026-09-24.
  • API-21 — SheetBody scrolls between the header and the footer, and a top or bottom sheet stops at max-h-[85dvh] so it can; SheetAction is an end seat in SheetHeader that clears the close button; closeLabel renames the corner close control.
  • OVL-18 — SheetFooter is a right-aligned wrapping row (flex flex-wrap items-center justify-end) at every width, not upstream's full-width flex-col stack; a direct child with data-slot="sheet-cancel" takes me-auto to 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 — SheetContent sets wrap-break-word, so a long unbroken word breaks inside the panel rather than running past its edge. While the corner × renders, a SheetHeader with no SheetAction takes pe-12, so a long title wraps before the × instead of running under it. With a SheetAction the header's text track is minmax(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 owns cn.
  • DOC-1 — prettier reflow plus the registry-stamp.mjs provenance header; no decision of their own.
  • OVL-22 — below the tablet tier (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.

On this page