Sheet
Extends the Dialog component to display content that complements the main content.
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
$ bext ui add sheet
Copy and paste the following into src/components/ui/sheet.tsx.
This component is interactive. Make sure the shared island is loaded (see Islands). It reacts to data-ui="sheet" hooks.
Usage
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.
Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.
On This Page