bx

Sheet

Extends the Dialog component to display content that complements the main content.

tsx
<Sheet>
  <SheetTrigger for="edit"><Button variant="outline">Edit</Button></SheetTrigger>
  <SheetContent id="edit" side="right">
    <SheetHeader>
      <SheetTitle>Edit profile</SheetTitle>
      <SheetDescription>Make changes to your profile.</SheetDescription>
    </SheetHeader>
    {/* form */}
  </SheetContent>
</Sheet>

Sheet is a side panel that slides in from an edge (right by default; top, left and bottom are also available). Used for: side-panel editing, advanced filters, an e-commerce cart, mobile navigation.

Compare with Dialog (centered modal) and Drawer (mobile bottom sheet with a drag handle). Sheet is the desktop format for variable-length content that does not break the workflow.

Installation

Terminal
$ bext ui add sheet

Usage

tsx
import { Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "@bext-stack/ui/primitives"

<Sheet>
  <SheetTrigger for="edit-sheet">
    <Button variant="outline">Edit</Button>
  </SheetTrigger>
  <SheetContent id="edit-sheet" side="right">
    <SheetHeader>
      <SheetTitle>Edit profile</SheetTitle>
      <SheetDescription>Make changes to your profile.</SheetDescription>
    </SheetHeader>
    {/* form fields */}
  </SheetContent>
</Sheet>

Anatomy

<Sheet>
  ├── <SheetTrigger for="sheet-id">
  │   └── <Button>Open</Button>
  └── <SheetContent id="sheet-id" side="right|left|top|bottom">
      ├── <SheetHeader>
      │   ├── <SheetTitle>...</SheetTitle>
      │   └── <SheetDescription>...</SheetDescription>
      └── ...free-form content...

API Reference

SheetTrigger

Prop Type Default Description
for string — Matches the id of the Content.

SheetContent

Prop Type Default Description
id string — Matches Trigger.for.
side "right" | "left" | "top" | "bottom" "right" Edge the panel slides in from.
ariaLabel string — Accessible name (aria-label) for a sheet that has no SheetTitle.
className string — Default: fixed with inset set by side, fixed w/h, bg-background border shadow.

SheetHeader

Prop Type Default Description
className string — flex flex-col gap-2, at the top of the content.

SheetTitle

Prop Type Default Description
id string — Optional id. Without it the island generates one to build aria-labelledby.
className string — <h2> text-lg font-semibold.

SheetDescription

Prop Type Default Description
id string — Optional id. Without it the island generates one to build aria-describedby.
className string — <p> text-sm muted-foreground.

Data Attributes

Attributes you can target in custom CSS to override the style for each state.

Attribute On Values Description
data-ui="open" Trigger data-target="#id" Opens on click and locks page scroll.
data-state Trigger "open" | "closed" Set by the island, together with aria-haspopup="dialog", aria-expanded and aria-controls on the button inside.
data-overlay Content (outer element) hidden by default Modal overlay: the island toggles its hidden attribute.
data-ui="close" corner × button, SheetClose — Closes the sheet and returns focus to the trigger.
data-ui="overlay-bg" backdrop — Clicking the backdrop closes the sheet.

Keyboard

Key Action
Enter / Space On the trigger: opens the sheet and moves focus to its first focusable element.
Tab / Shift+Tab Moves through the focusable elements of the sheet and wraps around. Focus cannot leave the sheet while it is open.
Escape Closes the sheet and returns focus to the trigger.

Accessibility

The panel has role="dialog" aria-modal="true". The island points aria-labelledby at the SheetTitle and aria-describedby at the SheetDescription, generating their ids from the content id when you give none. A sheet without a SheetTitle is named by its trigger button, or by the ariaLabel prop.

On open, focus moves to the first focusable element of the content (the corner close button only when there is nothing else). Tab and Shift+Tab are trapped inside. Escape, the close button or a backdrop click closes the sheet, and focus returns to the trigger. Page scroll is locked while the sheet is open.

Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.