Sheet
A panel that slides in from one edge of the viewport. Built on the
native <dialog> element with showModal() — top-layer rendering,
native focus trap, escape-to-close, inert background. Per the
answered design question recorded with this component: slide from
the chosen edge + fade backdrop; reduced-motion drops to fade-only.
When to use
- Settings or filters that benefit from a persistent, scrollable panel.
- Mobile menus that slide in from the side.
- Quick-action surfaces that need more room than a Popover but less ceremony than a Dialog.
Sides
data-side controls the entry edge:
| Value | Behavior |
|---|---|
right |
Slides in from the right (default) |
left |
Slides in from the left |
top |
Slides down from the top |
bottom |
Slides up from the bottom |
Anatomy
<dialog class="sheet" data-side="right">
<header class="sheet__header">
<h2>Filters</h2>
<button class="sheet__close" aria-label="Close" onclick="this.closest('dialog').close()">×</button>
</header>
<div class="sheet__body">
<!-- Field-groups, fields, etc. -->
</div>
<footer class="sheet__footer">
<button class="button -ghost" onclick="this.closest('dialog').close()">Cancel</button>
<button class="button -primary">Apply</button>
</footer>
</dialog>
<button onclick="document.querySelector('.sheet').showModal()">Open</button>
The runtime (initSheet, auto-registered for dialog.sheet) adds:
- Backdrop-click closes the sheet (opt-out via
closeOnBackdropClick: false). - Scroll lock on the document while open.
- Focus restoration to the previously-focused element on close.
Tokens used
--color-surface-raised— panel background--color-surface-overlay— backdrop--color-surface-canvas— close button hover--color-ink-strong,--color-ink-regular--color-accent— focus ring--color-border-muted— section dividers--space-inset-block-m,--space-inset-element-s--space-gap-elements-{s,m}--shadow-overlay— panel shadow--radius-button— close button radius--size-control-s— close button size--border-width-hairline/--border-width-focus--motion-base,--motion-easing-default
Component-tier (defined inline)
--sheet-size— overridable inline-size (right/left) or block-size (top/bottom)--sheet-duration— overridable enter/exit duration
Accessibility
Native <dialog> + showModal() carries:
- Top-layer rendering (above all other content).
- Focus trap (Tab cycles inside the sheet).
- Escape closes (calls
close()). - Background
inert(clicks and tab focus blocked behind the sheet).
The component adds:
- Backdrop-click close behavior.
- Focus restoration on close.
- Scroll lock on body so background does not scroll while open.
Authors should:
- Set a heading inside
.sheet__headerso assistive tech announces the sheet's purpose. - Give the close button
aria-label="Close"(the×glyph alone is not an accessible label).
Edge cases
- Reduced motion: the slide animation drops to a pure fade. The panel still appears from the correct edge logically; only motion differs.
- Full-bleed sheet on mobile:
max-inline-size: 100vwkeeps the sheet from overflowing narrow viewports. Override--sheet-sizefor a wider panel. - Top/bottom sheets: block-size is the relevant axis. Body content scrolls inside the sheet, not the page.
Do
- Use
<dialog>as the element;.sheetclass on it. - Pair
__closebutton witharia-label="Close". - Use
data-sideto pick the entry edge (default: right). - Call
dialog.showModal()to open;dialog.close()to close.
Don't
- Don't use
<div>with a custom modal implementation. The native<dialog>gives focus, escape, and top-layer for free. - Don't stack two open sheets at once. Use Dialog for nested modal cases.
- Don't disable scroll lock unless you have a reason — body scroll through the backdrop is disorienting.