jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

registry:ui · NativeHTML

sheet — a dialog that arrives from the side

A POSITIONING/ANIMATION variant of the native dialog, not a second state machine: showModal() (focus trap, Escape, top layer native), the same generation-token close path, bind:open lifecycle. The panel docks to an edge and slides in along its axis. Backdrop click is deliberately NOT wired — sheet content is often a form; a stray click shouldn't destroy it.

install — sheet

$npx jixoai-ui add sheet

https://ui.jixoai.com/r/sheet.json

4 sides no backdrop-click close same close path as dialog

overview

Overview

The side drawer on the dialog laws: a positioning/animation variant of the native <dialog>, not a second state machine — and the interior is the card kernel's.

sheet takes the platform's modal contract wholesale — showModal() supplies the top layer, the inert page behind, the focus trap, and Escape (as the cancel event) — and changes only where the panel sits and how it arrives: the panel docks to an edge (side) full-length and slides in along that axis from off-screen. The lifecycle is bind:open, the same contract as dialog; the close path is one generation token so a reopen during the slide-out supersedes the pending close. What sheet does NOT do is re-decide semantics: backdrop click is deliberately not wired (sheet content is often a form), and the footer is yours — CardFooter's carved action band is the standard face.

The delivery shape is the fleet's self-carried promotion: the §11 carriers stamp the <dialog> root itself, and the UA promotes THAT element to the top layer — the resolved axes ride the panel, never the trigger's tree. The interior is not sheet's own paint: it is the card kernel's three bands (head / body cell / optional foot) under the sheet's 18px rhythm escape hatch, with the × riding the kernel's end-action seat. The sheet owns the mechanism — edge docking, the slide state machine, the × glyph's scale — never the paint.

Two laws before you compose: the timing is a declared exception — the sheet keeps 200ms for entry, exit and the backdrop fade (the dialog family's law reads 120ms), and it is the family's last CSS-keyframe overlay pending its WAAPI kernel migration; and the modal contract is about focus, not scroll — the page behind is inert to focus and pointer interaction goes to the top layer, and wheel is swallowed at every hit target while the sheet is open (the page does not scroll; the scroller itself is never locked — programmatic scroll still works, nothing authors a lock). Lock it in your app shell if the drawer's content demands it.

sheet

Pick a side in the playground, then open: the panel slides from that edge. Escape or the × closes through the shared fade.

sheet-usage.svelte svelte
<Sheet bind:open title="Filters" {side} width="24rem">
  <!-- body -->
</Sheet>

Filters

Sheet body — a form, a list, anything. Focus is trapped; Escape and the × close.

Width demo

width defaults to the contract own 24rem; the drawer rides its own elevation level4 (8dp).

Compact

An explicit width lane and an explicit elevation lane — the §13 rename keeps size free for the scale axis.

Stamps

Four lanes on one drawer: the rung (data-density), the dark bridge, the concentric anchor (12px corners), and the root voice (18px).

law

The dialog laws, side-docked

The platform owns the modal contract; the sheet owns the mechanism — docking, the slide state machine, the generation-token close — and its 200ms is a declared exception, not a drift.

PropertyTypeDefaultDescription
the promotion showModal()the panel itself is the native <dialog> — top layer, inert page, UA focus trap and Escape-cancel come from the platform. The §11 carriers stamp the DIALOG ROOT (self-carried across the promotion — the resolved axes ride the panel, never the trigger's tree)announces: the page behind is inert; the drawer carries its own scope
the close path × · Escape · your footer · bind:openone generation token: every path lands in shut(); a reopen during the slide-out SUPERSEDES the pending close (the timer's gen check), so a fast toggle never loses the panelannounces: open always wins the race
the 200ms exception CLOSE_MS = 200the sheet keeps its own 200ms for entry, exit and the ::backdrop fade — a DECLARED exception to the dialog family's 120ms law (sheet.css keys --jx-surface-exit-ms: var(--motion-200)). It is also the family's r18 residue: the slide is a CSS keyframe state machine — the WAAPI kernel dialog adopted has not migrated here yetannounces: the last keyframe overlay — pending its kernel migration
the backdrop a click outside the panelNOTHING — backdrop click is deliberately not wired (sheet content is often a form; a stray click shouldn't destroy it). Close via ×, Escape, or your own footer actionannounces: measured: a backdrop click leaves the sheet open
reduced motion prefers-reduced-motion: reducethe state machine double-kills it: the CSS animation: none AND shut() skips the timer — dialog.close() lands instantly, no 200ms waitannounces: the panel is simply there, then gone
the interior header / children / footerthe card kernel's three bands carry the drawer: the head band (title face + the × on the end-action seat), the CardBody scroll cell under the sheet's 18px rhythm escape hatch, and the optional foot band — the footer snippet renders RAW, CardFooter's carved action band is the standard faceannounces: the sheet owns the mechanism (docking, slide, × scale), never the paint

types

Types

Four docking sides, a width along the docked axis, and the floating-surface paint.

side

left | right (default) | top | bottom — the panel docks full-length and slides in along its axis.

width

CSS length along the docked axis — 24rem default drawer extent; top/bottom panels cap at 85dvh. §13: the prop is width — a css width is never the scale axis.

variant

solid | acrylic | auto (default) — the floating-surface paint family, sliding on top of the surface rise.

usage

Usage

Backdrop click is deliberately not wired — sheet content is often a form; close via ×, Escape, or your own footer action.

Sheet usage
<script lang="ts">
  import Sheet from '@ui/sheet.svelte';
  import CardFooter from '@ui/card/card-footer.svelte';
  import PressButton from '@ui/press-button.svelte';
</script>

<PressButton onclick={() => (open = true)}>Filters</PressButton>
<Sheet bind:open title="Filters" side="right">
  <p>Filter controls — focus stays trapped, Escape closes.</p>
  {#snippet footer()}
    <!-- the carved action band: the cluster fills the foot band
         vertically, the rim Separator above is its top edge -->
    <CardFooter>
      <PressButton onclick={() => (open = false)}>Apply</PressButton>
    </CardFooter>
  {/snippet}
</Sheet>

a11y

Accessibility

The native dialog carries the modal contract; title is REQUIRED — it is the dialog's accessible name.

Keyboard

KeyAction
TabCycles the drawer's controls — the showModal() modal containment: no control behind the sheet can take focus (Chromium may visit the body for one hop between cycles, then re-enters)
EscapeCancel event, intercepted to share the 200ms animated close — focus restores to the trigger
Enter / SpaceActivate the focused control (× button, footer actions)

ARIA

AttributeValueDescription
aria-labeltitle (required)On the dialog element — the title is the REQUIRED accessible name.
roledialog (native)The platform element; focus containment, inert page and top layer are native.
aria-label"Close"On the × button.

api

API

Eight props; the same bind:open lifecycle and close path as dialog.svelte.

Properties

PropertyTypeDefaultDescription
open bindbooleanfalseBindable open state — same contract as dialog.svelte.
side 'left' | 'right' | 'top' | 'bottom''right'The edge the panel docks to; slides along that edge’s axis.
title * string—REQUIRED a11y: the dialog’s name (aria-label target).
children * Snippet—Panel body.
header Snippet—Optional header row content beyond the title + ×.
footer Snippet—Optional sticky footer action row.
width string'24rem' · Own default, not ambientDrawer extent along the docked axis (CSS length). §13 RENAME (W3-C): the prop was `size` — a css width is not the scale axis; the freed name belongs to the universal size lane (root font-size).
variant 'solid' | 'acrylic' | 'auto''auto' · Own default, not ambientFloating-surface paint. Defaults: literal slot — own ’auto’, ambient when an axis opens.

Universal props

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity across the motion kernels (§8)

axes

The eight axes on sheet

The self-carried promotion: every resolved lane lands on the <dialog> root (carriers + the §3/§7 consumption pairs + --jx-sheet-size), and the top layer carries the scope — the page behind gets nothing. Seven axes are no-own; elevation is the family's ONE declared own (level4).

PropertyTypeDefaultDescription
density 'small' | 'medium' | 'large' | 'auto' | number (+ the five legacy spellings)'auto'SELF-CARRIED, RUNG STAMP — the resolved lane lands on the dialog root, which stamps the §4 legacy rung (measured: density="small" → data-density="sm" — today's CSS keys on the rungs); the top layer carries the scope for everything the drawer renders. Number unit: coefficient.
size 'small' | 'medium' | 'large' | 'auto' | number'auto'SELF-CARRIED — the §11 pair (--jx-size-effective + font-size) lands on the dialog root (measured 18px computed on the size demo); the body cell's own text step is 13px and stays the cell's unless the lane overrides the root. Number unit: px.
shape 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto'SELF-CARRIED, ALIAS LADDER — the lane stamps --jx-shape-effective (+ the §14 radius factor) on the root; sheet.css's silhouette rule reads corner-shape: var(--jx-shape-effective, round). Number unit: none.
radius 'small' | 'medium' | 'large' | 'auto' | number'auto'SELF-CARRIED, THE CONCENTRIC ANCHOR — an explicit lane stamps --jx-radius-effective on the top-layered root and the consumed calc paints the silhouette (measured: radius={12} → 12px computed border-radius); 'auto' consumes the broadcast against the panel's OWN ANCESTORS — the declaration site in your tree, not the trigger's. Number unit: px.
color 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto'SELF-CARRIED, SUPPLY INTO THE TOP LAYER — --jx-color-effective lands on the root; the drawer's own chrome is inked popover-foreground (the frame atom) and reads no hue directly, so the lane's work is the scope it supplies to what you render inside. Number unit: hue degrees.
theme 'light' | 'dark' | 'system' | 'auto''auto'SELF-CARRIED, THE BRIDGE — class:dark lands on the dialog root (measured), and because the whole card-dialect interior is DOM-descended from that root, the entire drawer re-voices inside the top layer — bands, separators, ink. No number lane.
elevation 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'level4' · the family's ONE declared ownOWN, level4 — the drawer's historic z-feel (8dp, the dialog rung it docks beside), the only axis with an opinion in SheetDefaults. An explicit lane overrides it (the carried level2 demo measures the swap); the §7 consumption pair stamps the shadow recipe + the paired ladder-rung surface on the root. Number unit: dp.
motion 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto'SUPPLIED, NOT YET READ BY THE SLIDE — the carrier lands on the root, but the r18 keyframe state machine reads --motion-200/--motion-ease-nav directly, not the motion lane (the kernel migration is pending). The lane that DOES bite: prefers-reduced-motion kills the slide entirely (CSS + the instant-close path). Measured in pixels below. Number unit: coefficient.
TokenDefaultSource
--jx-sheet-size24rem (width prop)component
the slide keyframessheet.css, unlayered :wherestructural
::backdropvar(--scrim)structural
.jx-sheet-body-cellthe rhythm escape hatchcomponent
.jx-sheet-x svg14px descendant scalecomponent
data-jx-card hostcard.css importedstructural

Sheet · universal props

sheet-universal.svelte svelte
<!-- the §13 rename: a css WIDTH is 'width'; 'size' is the scale axis -->
<Sheet bind:open title="Filters" side="right" width="24rem">…</Sheet>
<!-- the §7 pair: the drawer's own level4, an explicit lane steps it -->
<Sheet bind:open title="Compact" width="18rem" elevation="level2">…</Sheet>
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h