Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Sheet

A dialog that slides in from a screen edge — four sides, a header/footer layout, focus trapping, and an animated slide transition.

Status
stable
Since
0.1.0
Accessibility pattern
APG dialog (modal)

Last updated

Install

Add Sheet from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/sheet

The same command installs the registry items it composes: @vegastack/icon-button, @vegastack/use-modal-inert.

Usage

import {
  Sheet,
  SheetTrigger,
  SheetContent,
  SheetHeader,
  SheetFooter,
  SheetTitle,
  SheetDescription,
  SheetClose,
} from "@/components/ui/sheet";
import { Button } from "@/components/ui/button";

<Sheet side="right">
  <SheetTrigger render={<Button variant="outline">Edit profile</Button>} />
  <SheetContent>
    <SheetHeader>
      <SheetTitle>Edit profile</SheetTitle>
      <SheetDescription>Make changes to your profile here.</SheetDescription>
    </SheetHeader>
    <SheetFooter>
      <SheetClose render={<Button variant="outline">Cancel</Button>} />
      <Button>Save changes</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>;

Anatomy

Sheet is a compound component built on Base UI's Dialog. Compose the parts inside the root:

Sheet
SheetClose — data-slot="sheet-close-action"
SheetContent — data-slot="sheet-backdrop" | "sheet-body" | "sheet-close" | "sheet-content" | "sheet-viewport"
SheetDescription — data-slot="sheet-description"
SheetFooter — data-slot="sheet-footer"
SheetHeader — data-slot="sheet-header"
SheetProvider
SheetTitle — data-slot="sheet-title"
SheetTrigger — data-slot="sheet-trigger"
SheetVirtualKeyboardProvider
<Sheet side="right">
  <SheetTrigger render={<Button>Open</Button>} />
  <SheetContent size="md">
    <SheetHeader>
      <SheetTitle>Title</SheetTitle>
      <SheetDescription>Supporting description text.</SheetDescription>
    </SheetHeader>
    {/* body content */}
    <SheetFooter>
      <SheetClose render={<Button variant="outline">Cancel</Button>} />
      <Button>Confirm</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>
  • Sheet — the root; owns open/close state (open / defaultOpen / onOpenChange) and the side. Modal by default — focus is trapped and page scroll is locked. Built on Base UI's Drawer, so snapPoints / snapPoint / onSnapPointChange pass straight through.
  • SheetTrigger — the control that opens the sheet (data-slot="sheet-trigger"). Pass render to compose it with a Button.
  • SheetContent — the slide-in panel (data-slot="sheet-content"). Composes Base UI Drawer's Portal + Backdrop + Viewport + Popup + Content, takes the size, slides enter/exit, and renders the top-end close button.
  • SheetProvider — groups sibling sheets so nested panels animate as one stack. A single sheet does not need it.
  • SheetVirtualKeyboardProvider — opts a bottom sheet containing form fields into Base UI's software-keyboard handling.
  • SheetHeader — groups the title and description (data-slot="sheet-header").
  • SheetTitle — the sheet's accessible name (data-slot="sheet-title", <h2>). Wired to the popup via aria-labelledby.
  • SheetDescription — supporting text (data-slot="sheet-description", <p>). Wired via aria-describedby.
  • SheetFooter — the action row pinned to the bottom (data-slot="sheet-footer").
  • SheetClose — closes the sheet (data-slot="sheet-close-action"). Pass render to compose it with a Button (e.g. a "Cancel" action).

Sheet vs. Dialog: reach for a Sheet when the panel should anchor to a screen edge — filters, detail panels, secondary forms, navigation. Use a Dialog for a centered, focused confirmation or short form.

Examples

Sides

Sheet takes a side prop controlling which edge the panel pins to, slides in from, and is swiped towards to dismiss: top, right (default), bottom, and left. left/right pin full-height and size their width; top/bottom span full-width and cap their height.

side lives on the root, not on SheetContent: it selects Base UI's swipeDirection as well as the pinned edge, and a side that lived on the content could disagree with the gesture.

Sizes

SheetContent takes sizesm, md (default), lg, full — from the shared panel-width vocabulary. One tier means one thing in both axes: on a left/right sheet it is the panel's width, on a top/bottom sheet its height. Reach for it instead of a className width override.

SheetContent also exposes two close-button props: showCloseButton={false} drops the top-end X (force dismissal through a footer action, Esc, or a swipe), and closeLabel relabels that button for screen readers.

{
  /* No X button — dismiss only via an explicit action */
}
<SheetContent showCloseButton={false}>{/* … */}</SheetContent>;

{
  /* Wider panel + a custom close-button label */
}
<SheetContent size="lg" closeLabel="Dismiss filters">
  {/* … */}
</SheetContent>;

Playground

Choose the edge the panel slides in from and toggle the 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="outline">Cancel</Button>} />
      <Button>Save changes</Button>
    </SheetFooter>
  </SheetContent>
</Sheet>

API Reference

SheetContent

PropTypeDefaultDescription
closeLabelstring"Close"Accessible label for the close button.
showCloseButtonbooleantrueRender the top-end close (X) button.
sizeSheetSize"md"The panel's extent along its free axis — width for a left/right sheet, height for a top/bottom one. full spans the viewport.

Data attributes and CSS variables on SheetContent

AttributeValues
data-sidemirrors a prop or state value
data-sizemirrors a prop or state value
data-slot"sheet-backdrop" | "sheet-body" | "sheet-close" | "sheet-content" | "sheet-viewport"

Sheet, SheetTrigger, SheetTitle, SheetDescription, and SheetClose add no props of their own — each accepts everything the corresponding Base UI Dialog part accepts (Root, Trigger, Title, Description, Close): open / defaultOpen / onOpenChange on the root, render on the trigger and close, plus className and ref. SheetHeader and SheetFooter are plain layout wrappers that accept standard <div> props.

Accessibility

  • The panel renders with role="dialog" + aria-modal="true". SheetTitle and SheetDescription are auto-wired as its aria-labelledby / aria-describedby — always include a title.
  • Focus is trapped inside the panel while open and restored to the trigger on close. For the default modal, VegaStack mirrors Base UI's live outside-tree markers to native inert, shared with Dialog and AlertDialog through reference-counted ownership. modal={false} and modal="trap-focus" retain their Base UI outside-interaction contracts. Page scroll is locked.
  • The close button is a native button with an accessible label (closeLabel, default "Close"); meaning of the X icon is carried by that label. On hover it shifts to bg-muted text-foreground, and the browser's default focus outline is preserved (no outline: none).
  • The trigger and footer actions render as native buttons (or your composed Button via render), so they keep their own focus-visible treatment — the panel itself never suppresses focus indicators.
  • Swipe-to-dismiss is an addition, never the only way out: Esc, the close button, and a backdrop press all still close the sheet, so a pointer gesture is never required.
  • Wrap a bottom sheet containing form fields in SheetVirtualKeyboardProvider so the software keyboard does not cover the focused field.
KeyAction
Enter / SpaceActivate the focused trigger to open the sheet.
Tab / Shift + TabMove focus between controls — trapped within the open sheet.
EscClose the sheet.
ContractStates tested
Behaviourdefault, open
Accessibilitydescribed, focus-visible, labeled, semantic-html
Visualdefault, hover

Do / Don't

Do
Always include a SheetTitle so the sheet has an accessible name, and pick a side that matches the panel's purpose — right/left for forms and detail, top/bottom for navigation or short pickers.
Don't
Cram a long, multi-step flow into a Sheet — route to a dedicated page instead, and don't stack multiple sheets at once.

On this page