Sheet
elements man ui/templates/sheetA dialog that slides in from an edge of the screen, for content that complements the page.
Exampleimport { Sheet, SheetBody, SheetFooter, SheetClose } from "@elements/ui/sheet"; import { Button } from "@elements/ui/button"; <Sheet side="right" title="Edit profile" description="Make changes, then save."> <Button slot="trigger">Edit profile</Button> <SheetBody> <input value={form.name}> </SheetBody> <SheetFooter> <Button variant="primary" onclick={() => save(form)}>Save changes</Button> <SheetClose><Button>Close</Button></SheetClose> </SheetFooter> </Sheet> <!-- controlled: open is two-way --> <Sheet open={state.filters} side="left" title="Filters">...</Sheet>
Parts
| Part | What it is |
|---|---|
Sheet |
The root: owns open, renders the trigger slot and, while open, the <dialog> overlay, the backdrop and the docked panel. |
SheetHeader |
Stacks a title and description. |
SheetTitle |
An <h2>; names the sheet (aria-labelledby). |
SheetDescription |
A <p>; describes the sheet (aria-describedby). |
SheetBody |
The content between header and footer; it scrolls when the content is taller than the sheet, so the header and footer stay in place. |
SheetFooter |
Actions pinned to the bottom, stacked full width. |
SheetClose |
Wraps any element; clicking it closes this sheet. |
Attributes
Sheet
| Attribute | Type | Default | Description |
|---|---|---|---|
open |
boolean (two-way) |
false |
Whether the sheet is open. Bind it to control it; the sheet writes false back when it closes itself. Omit it and the trigger slot opens it. |
side |
"top" | "right" | "bottom" | "left" | "start" | "end" |
"right" |
The edge the sheet docks to and slides from. start and end follow the writing direction (end is the left edge under rtl). |
title |
string |
"" |
Renders a SheetTitle in a header and names the sheet. |
description |
string |
"" |
Renders a SheetDescription under the title. |
label |
string |
"" |
aria-label for a sheet with no visible title. |
modal |
boolean | "trap-focus" |
true |
true: backdrop, inert page, focus trap, scroll lock. "trap-focus": focus is trapped but there is no backdrop or lock, and the page takes clicks. false: the page stays usable. |
dismissOnEscape |
boolean |
true |
Escape closes the sheet. |
dismissOnOutside |
boolean |
true |
A press outside the panel closes the sheet. |
closeButton |
boolean |
true |
Shows the X button in the corner. |
initialFocus |
DialogFocus |
"" |
CSS selector of the element to focus on open, or a function of the interaction type (keyboard, mouse, touch, pen) that returns an element, a selector, true for the default or false to leave focus. Default: [autofocus], else the first focusable element that is not a link, else the panel. |
returnFocus |
boolean |
true |
Return focus to the trigger (or whatever had focus) when the sheet closes. |
finalFocus |
DialogFocus |
"" |
CSS selector of the element to focus on close, in place of the trigger, or a function of the interaction type that returns an element, a selector, true for the trigger or false to leave focus. |
size |
"sm" | "md" | "lg" | "xl" | "full" |
"md" |
Max width of a side sheet: 20, 24, 36, 48rem or the full viewport. A side sheet is 75% of the viewport up to that width; top and bottom sheets span the width. |
scroll |
"inside" | "outside" |
"inside" |
inside: the sheet fits its edge and SheetBody scrolls. outside: the sheet grows with its content and the overlay scrolls it as one (an outside scroll dialog); a side sheet is at least the viewport's height. |
class |
string |
"" |
Extra classes on the panel. |
onOpenChangeComplete |
(open: boolean) => void |
() => {} |
Called after the enter animation (true) or once the sheet has left the page after its exit (false). |
onOpenChange |
(open: boolean, reason?: DialogChangeReason, details?: DialogChangeDetails) => void |
() => {} |
Called when the sheet opens or closes itself, with a reason: trigger-press, outside-press, escape-key, close-press, focus-out, close-watcher (a platform close request such as Android back), or none when the browser closed it. Not called for changes the caller makes. The third argument is the event details: { reason, event (the DOM event behind the change, or null), trigger (the trigger element on trigger-press) }. |
onInteractOutside |
(e: Event) => void |
() => {} |
Called before a press or focus outside dismisses it; e.preventDefault() keeps it open. |
onOpenAutoFocus |
(e: Event) => void |
() => {} |
Called before focus moves in on open; e.preventDefault() leaves focus to the handler. |
onCloseAutoFocus |
(e: Event) => void |
() => {} |
Called before focus returns on close; e.preventDefault() leaves focus to the handler. |
onEscapeKeyDown |
(e: KeyboardEvent) => void |
() => {} |
Called on Escape before closing; e.preventDefault() keeps it open, and e.allowPropagation() lets the Escape reach the page's own key handlers too. |
onPointerDownOutside |
(e: PointerEvent) => void |
() => {} |
Called on a press outside before closing; e.preventDefault() keeps it open. |
SheetHeader, SheetTitle, SheetDescription, SheetBody, SheetFooter
| Attribute | Type | Default | Description |
|---|---|---|---|
class |
string |
"" |
Extra classes. |
Slots
| Slot | Content |
|---|---|
trigger |
The element that opens the sheet. It gets aria-haspopup, aria-expanded, aria-controls and data-state. |
default |
The sheet's content, below the header. |
Keyboard
| Keys | Action |
|---|---|
Space / Enter |
On the trigger: opens the sheet. |
Tab |
Moves to the next focusable element inside; wraps from the last to the first. |
Shift + Tab |
Moves to the previous focusable element; wraps from the first to the last. |
Escape |
Closes the sheet (unless dismissOnEscape is false or onEscapeKeyDown prevents it). |
Data attributes
| Attribute | Values | On | Description |
|---|---|---|---|
data-open |
present | .dialog-overlay, .sheet-backdrop, .sheet | While open; the same moment as data-state="open". |
data-closed, data-ending-style |
present | .dialog-overlay, .sheet-backdrop, .sheet | During the exit animation; the same moment as data-state="closed". |
data-starting-style |
present | .dialog-overlay, .sheet-backdrop, .sheet | For the first frame after the overlay mounts, so a CSS transition can start from it. |
data-popup-open |
present | trigger | While open; the same moment as the trigger's data-state="open". |
data-state |
"open" | "closed" |
trigger, .dialog-overlay, .sheet-backdrop, .sheet | closed during the exit animation. |
data-side |
"top" | "right" | "bottom" | "left" | "start" | "end" |
.sheet | |
data-modal |
"true" | "false" |
.dialog-overlay | |
data-scroll-locked |
present | <body> |
While a modal sheet is open. |
data-sheet-scroll-stop |
present | .sheet-body | The body overflows and is a Tab stop (tabindex 0), a region named after the title. |
data-nested |
present | .sheet | The sheet sits inside another dialog panel. |
data-nested-dialog-open |
present | .sheet | A dialog opened from inside this one is open. --nested-dialogs holds the count. |
Accessibility
- The overlay is a native
<dialog>opened with showModal(): role dialog, the top layer, and an inert page behind it. - aria-modal="true", aria-labelledby (the title) and aria-describedby (the description) are set on it.
- Focus moves into the sheet on open, is trapped while modal, and returns to the trigger on close.
- The close button is an icon button with aria-label="Close".
See also
- Button (opens in a new tab): A button or a link that looks like one, in six variants and four sizes.
- AlertDialog (opens in a new tab): A modal that interrupts with something important and waits for an answer.
- Drawer (opens in a new tab): A panel that slides from an edge and can be dragged away, with snap points and a scaled background.