Sheet
A dialog that slides in from a screen edge — four sides, a header/footer layout, focus trapping, and an animated slide transition.
- Status
- 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/sheetThe 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 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 theside. Modal by default — focus is trapped and page scroll is locked. Built on Base UI'sDrawer, sosnapPoints/snapPoint/onSnapPointChangepass straight through.SheetTrigger— the control that opens the sheet (data-slot="sheet-trigger"). Passrenderto compose it with aButton.SheetContent— the slide-in panel (data-slot="sheet-content"). Composes Base UI Drawer's Portal + Backdrop + Viewport + Popup + Content, takes thesize, 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 viaaria-labelledby.SheetDescription— supporting text (data-slot="sheet-description",<p>). Wired viaaria-describedby.SheetFooter— the action row pinned to the bottom (data-slot="sheet-footer").SheetClose— closes the sheet (data-slot="sheet-close-action"). Passrenderto compose it with aButton(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 size — sm, 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
| Prop | Type | Default | Description |
|---|---|---|---|
closeLabel | string | "Close" | Accessible label for the close button. |
showCloseButton | boolean | true | Render the top-end close (X) button. |
size | SheetSize | "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
| Attribute | Values |
|---|---|
data-side | mirrors a prop or state value |
data-size | mirrors 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".SheetTitleandSheetDescriptionare auto-wired as itsaria-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}andmodal="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 theXicon is carried by that label. On hover it shifts tobg-muted text-foreground, and the browser's default focus outline is preserved (nooutline: none). - The trigger and footer actions render as native buttons (or your composed
Buttonviarender), 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
SheetVirtualKeyboardProviderso the software keyboard does not cover the focused field.
| Key | Action |
|---|---|
| Enter / Space | Activate the focused trigger to open the sheet. |
| Tab / Shift + Tab | Move focus between controls — trapped within the open sheet. |
| Esc | Close the sheet. |
| Contract | States tested |
|---|---|
| Behaviour | default, open |
| Accessibility | described, focus-visible, labeled, semantic-html |
| Visual | default, hover |