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

registry:ui · NativeHTML

dialog — the platform owns the hard parts

One native <dialog> element, opened with showModal() and closed with close(). Focus trapping, the inert page behind, top-layer rendering above every sticky header, and the Escape key are browser features — the component only binds open state to them and adds the shared WAAPI surface timeline. Closed dialogs render nothing in the page, with or without JavaScript. Since the structural kernel (2026-09-09) the interior is the CARD DIALECT: the head/body/foot bands, separators, inline ruler and narrow reversal all ride the data-jx-card sticker (card.css), the faces are the Card family parts (<CardHeader>, <CardBody>, <CardFooter> — the retired DialogHeader/DialogFooter were their clones), and the × rides the end-action seat the card sources reserved for it. The dialog owns the mechanism only: material, scrim, motion, close contract.

install — dialog

$npx jixoai-ui add dialog

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

<dialog> + showModal() focus trap · inert · top layer ::backdrop 14% brand Escape → cancel 460ms WAAPI surface timeline footer buttons auto-group · ghost

overview

Overview

One native <dialog>, one motion addition: the platform owns the modal mechanics (top layer, Escape, closed-by-default, the scrim token), the component binds open state and drives the 460ms surface kernel, and the interior is the card dialect — the head/body/foot bands ride the data-jx-card sticker and the Card family faces.

The mechanism split is the component's design rule: everything the browser ships is consumed as-is — showModal() lifts the panel into the top layer above every sticky header, the cancel event carries Escape, form method="dialog" closes natively, and a closed dialog renders nothing inline (no-JS loads included). The component adds exactly two things: bindable open state (rising edge → showModal(), falling edge → the teardown) and the shared 460ms --jx-p WAAPI kernel — entry, exit, and the scrim ride one progress property, instant under reduced motion.

The interior is not dialog flesh: since the structural kernel the panel grows the CARD DIALECT — the interior host stamps data-jx-card and card.css's rule set applies (the five-column inline ruler, three integer bands, the footer's narrow reversal), the faces are CardHeader / CardBody / CardFooter (the head band is unconditional — the × contract — the foot band exists iff the footer snippet is passed), and the ghost variant scopes are written over both zones. The dialog owns the mechanism only: material (variant), scrim, motion, close contract.

The measured contract on the served page: the surface width is min(92vw, 26rem) (416px at desktop, 368px at a 400px viewport — responsive, measured); the scrim is achromatic --scrim (black 32% in light, white 10% in dark — never a brand tint; the staged demos pin light); the platform focus trap holds for the in-dialog cycle but LEAKS every second Tab to the page's skip link (measured per-press by two reviewers — the repair is queued, W-next #17); and the elevation axis is OWN level4 but STAMPED, NOT PAINTED (W-next #18) — the one axis lane that paints through the top layer is radius, the §3 concentric anchor (a 20px seat computes 6px on an auto child, digit-exact). Kinship: system-dialog (the non-modal, popover-based cousin), sheet (the edge-anchored twin), popover (the anchor-positioned non-modal surface).

dialog

One native <dialog> driven by showModal(): the browser owns Escape and the top layer — the component adds bindable open state and the shared WAAPI surface timeline. The platform focus trap holds for the in-dialog cycle but LEAKS every second Tab to the page's skip link (measured per-press; the repair is the family's, W-next). Retitle it from the Playground; preview the scrim in both stage themes.

Deploy queued

build #128 is waiting for a runner. The log streams once it picks up.

dialog-usage.svelte svelte
<script lang="ts">
  import Dialog from '@ui/dialog.svelte';
  import PressButton from '@ui/press-button.svelte';
</script>

let open = $state(false);
let title = $state('Deploy queued');
</script>

<PressButton onclick={() => (open = true)}>Open dialog</PressButton>

<Dialog {title} bind:open>
  <p>build #128 is waiting for a runner. The log streams once it picks up.</p>
</Dialog>

demo

Basic

A PressButton flips a bindable open state; the dialog does the rest. Try the × button, the Escape key, and Tab — the in-dialog cycle holds and the page behind is inert, with ONE measured exception: every second Tab reaches the page's skip link (the one page focusable outside the modal's inertness; the next press returns — W-next #17).

dialog · basic

state: open = false

Deploy queued

build #128 is waiting for a runner. The log streams once it picks up.

dialog-basic-demo.svelte svelte
<script lang="ts">
  import Dialog from '@ui/dialog.svelte';
  import PressButton from '@ui/press-button.svelte';
</script>

let open = $state(false);
</script>

<PressButton onclick={() => (open = true)}>Open dialog</PressButton>

<Dialog title="Deploy queued" bind:open>
  <p>build #128 is waiting for a runner. The log streams once it picks up.</p>
</Dialog>

Every exit — ×, Escape, or setting open = false from code — runs the same 460ms surface timeline before the real close(). Reduced-motion users get the instant close.

usage
<script lang="ts">
  import Dialog from '@ui/dialog.svelte';
  import PressButton from '@ui/press-button.svelte';
</script>

let open = $state(false);
</script>

<PressButton onclick={() => (open = true)}>Open dialog</PressButton>

<Dialog title="Deploy queued" bind:open>
  <p>build #128 is waiting for a runner. The log streams once it picks up.</p>
</Dialog>

demo

With a footer — form type

The footer snippet is the RAW override of the foot zone — and its standard content is <CardFooter>: the buttons passed as children auto-join one button-group at the row's inline end, ghost by default through the Dialog's zone scope, an explicit fill still winning for the primary. Cancel drops the dialog; Confirm does its work first, then closes through the same animated path. The form shells inside are engraved WELLS (the entity law, r14-12): the dialog is the one solid object — the inputs keep their crisp hairline edge while their ground dissolves into the panel's surface, the well inset carrying the depth; focus still tints, hover still deepens.

last action: —

Rotate API key

Minting a new key revokes the current one after 24 hours.

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

let open = $state(false);

const confirm = () => {
  open = false;
  // ...rotate the key
};
</script>

  <Dialog title="Rotate API key" bind:open>
  <p>Minting a new key revokes the current one after 24 hours.</p>
  {#snippet footer()}
    <CardFooter>
      <PressButton onclick={() => (open = false)}>Cancel</PressButton>
      <PressButton variant="fill" onclick={confirm}>Rotate key</PressButton>
    </CardFooter>
  {/snippet}
</Dialog>

demo

CardHeader — a custom head

The head snippet replaces the visible title row, and CardHeader is its content face: children ride flush, edge-to-edge — the content owns the row's height and padding (an Input shell brings its own), no zone insets intervening. The × close button still rides the head grid's end slot, and title keeps naming the dialog for assistive tech even though its visual row is gone — the search palette composes this same seam.

dialog · custom head

query: —
  • runner picked up job #128
  • cache restored in 412ms
  • artifact uploaded: dist.tape
  • lint clean — 0 warnings
  • unit tests 61/61
  • snapshot written (2.3 KB)
  • deploy probe: healthy
  • audit trail sealed
dialog-head-demo.svelte svelte
<script lang="ts">
  import Dialog from '@ui/dialog.svelte';
  import CardHeader from '@ui/card/card-header.svelte';
  import Input from '@ui/input.svelte';
  import Icon from '@ui/icon.svelte';
</script>

let open = $state(false);
let query = $state('');
</script>

<Dialog title="Filter events" bind:open>
  {#snippet head()}
    <!-- col-start-1 = the flush escape: the face's column start
         pins to the grid's first line while the x seat keeps its
         own column on the same row -->
    <CardHeader class="col-start-1">
      <Input class="w-full min-w-0" placeholder="Filter events…" bind:value={query} aria-label="Filter events">
        {#snippet innerInlineStart()}
          <span class="flex-none select-none text-muted-foreground" aria-hidden="true"><Icon name="search" /></span>
        {/snippet}
      </Input>
    </CardHeader>
  {/snippet}
  <!-- the body lists the events filtered by query -->
</Dialog>
custom head
<script lang="ts">
  import Dialog from '@ui/dialog.svelte';
  import CardHeader from '@ui/card/card-header.svelte';
  import Input from '@ui/input.svelte';
  import Icon from '@ui/icon.svelte';
</script>

let open = $state(false);
let query = $state('');
</script>

<Dialog title="Filter events" bind:open>
  {#snippet head()}
    <!-- col-start-1 = the flush escape: the face's column start
         pins to the grid's first line while the x seat keeps its
         own column on the same row -->
    <CardHeader class="col-start-1">
      <Input class="w-full min-w-0" placeholder="Filter events…" bind:value={query} aria-label="Filter events">
        {#snippet innerInlineStart()}
          <span class="flex-none select-none text-muted-foreground" aria-hidden="true"><Icon name="search" /></span>
        {/snippet}
      </Input>
    </CardHeader>
  {/snippet}
  <!-- the body lists the events filtered by query -->
</Dialog>

demo

Scrolling body — head and foot stay pinned

The panel itself never scrolls: the scroll ring is a row-ruled grid (head · separator · body · separator · foot) under a height cap, and the body zone is the only scroll environment — its scrollbar rides the zone edge with a stable both-edges gutter while the header bar and footer cluster stay pinned. The scroll itself is DECLARATIVE: scroll=false asserts the body fits — the scroll authority and the gutter reservation retire together, the content keeps its full width. The class prop here caps the ring (a geometry-only override) so the scroll shows even on tall viewports.

dialog · scrolling body

Event log

  1. 1 boot — kernel 6.9.4-arm64, 4 workers
  2. 2 mount /workspace — 12 GB free
  3. 3 env: node 22, pnpm 9, playwright 1193
  4. 4 restore cache — hit (412ms)
  5. 5 plan: apps/www build + 3 verify jobs
  6. 6 step 1/6 — typecheck
  7. 7 svelte-check 0 errors, 0 warnings
  8. 8 step 2/6 — unit (vitest)
  9. 9 dialog-grid.spec — 12 passed
  10. 10 dialog-ghost-scope.spec — 9 passed
  11. 11 search-client.spec — 14 passed
  12. 12 print-freeze.spec — 34 passed
  13. 13 step 3/6 — mirror gate
  14. 14 mirror manifest — 2 files in sync
  15. 15 step 4/6 — deps closure
  16. 16 registry closure — clean
  17. 17 step 5/6 — build site
  18. 18 vite build — 99 pages, 2 noindex skipped
  19. 19 search corpus — 832 sections indexed
  20. 20 step 6/6 — verify print
  21. 21 chromium 1193 launched
  22. 22 pagedjs preview — 5 pages
  23. 23 line-rhythm probe — nominal
  24. 24 zero-rerun probe — renderId stable
  25. 25 teardown — artifacts kept
  26. 26 done in 214s
  27. 27 publish — registry payload 1.8 MB
  28. 28 cdn purge — 3 edges
  29. 29 probe https://ui.jixoai.dev — 200
  30. 30 probe /r/registry.json — 200
  31. 31 probe /search/corpus.json — 200
  32. 32 smoke: dialog demo opens
  33. 33 smoke: footer clusters render
  34. 34 smoke: custom head filters
  35. 35 smoke: scrolling body pins zones
  36. 36 nightly anchor — queued
  37. 37 sleep until 02:00 UTC

Session pinned

3 rules active · 2 breakpoints · no stack frames. The body declares itself a non-scroller — nothing to roll, no gutter to reserve.

dialog-scroll-demo.svelte svelte
<script lang="ts">
  import Dialog from '@ui/dialog.svelte';
  import CardFooter from '@ui/card/card-footer.svelte';
  import PressButton from '@ui/press-button.svelte';
</script>

let open = $state(false);
</script>

<Dialog
  title="Event log"
  bind:open
  class="[&_[data-jx-card]]:max-h-[22rem]"
>
  <ol class="flex flex-col gap-1 font-mono text-[12px]">
    {#each lines as line, i}
      <li class="flex gap-3"><span class="text-muted-foreground">{i + 1}</span>{line}</li>
    {/each}
  </ol>
  {#snippet footer()}
    <CardFooter>
      <PressButton onclick={() => (open = false)}>Close</PressButton>
      <PressButton variant="fill" onclick={exportLog}>Export log</PressButton>
    </CardFooter>
  {/snippet}
</Dialog>
scrolling body
<script lang="ts">
  import Dialog from '@ui/dialog.svelte';
  import CardFooter from '@ui/card/card-footer.svelte';
  import PressButton from '@ui/press-button.svelte';
</script>

let open = $state(false);
</script>

<Dialog
  title="Event log"
  bind:open
  class="[&_[data-jx-card]]:max-h-[22rem]"
>
  <ol class="flex flex-col gap-1 font-mono text-[12px]">
    {#each lines as line, i}
      <li class="flex gap-3"><span class="text-muted-foreground">{i + 1}</span>{line}</li>
    {/each}
  </ol>
  {#snippet footer()}
    <CardFooter>
      <PressButton onclick={() => (open = false)}>Close</PressButton>
      <PressButton variant="fill" onclick={exportLog}>Export log</PressButton>
    </CardFooter>
  {/snippet}
</Dialog>

W3C foundation

What the platform gives, what we add

The design rule for this component: every behavior the browser ships is consumed as-is; the component only owns state binding and one motion. Anything beyond that is a named extension direction, not hidden magic.

platform-native, free

  • showModal() — top-layer rendering, focus trap, inert background
  • ::backdrop — the scrim pseudo-element; --scrim: semi-transparent black in light mode, white in dark mode — a scrim dims/lightens, never colors
  • Escape — the cancel event, intercepted only to share the fade
  • closed by default — no-JS page loads never paint dialog content inline
  • form method="dialog" — footer submits close natively (instant, skips the fade)

jixoai additions & extensions

  • bind:open — rising edge calls showModal(), falling edge runs the teardown
  • surface timeline — the single motion addition: the 460ms --jx-p kernel drives entry, exit, and the scrim; instant under reduced motion
  • floating-surface law — the hard offset shadow is a REAL ::after layer; @starting-style entry pulls the layers apart, the close fade presses them back; variant="solid | acrylic | auto" paints the surface (acrylic = dual-layer backdrop-filter)
  • × close button — press physics, right of the header bar
  • extension: backdrop-click close; intercepting form submits so they fade too

types

Dialog variants

Title and footer are the two compositional axes; variant paints the surface.

titled

The header bar renders when title is given — heading left, × close right.

chrome-less / footer

Omit title for a bare body; the footer snippet adds the separator-bounded foot zone — its standard content is CardFooter, whose buttons auto-join one end-packed group, ghost by default.

variant

solid | acrylic | auto (default) — acrylic is a dual-layer backdrop-filter, auto defers to the environment's transparency preference.

usage

Usage

Flip bind:open from anywhere — every exit (×, Escape, code) runs the same animated close.

Dialog usage
<script lang="ts">
  import Dialog from '@ui/dialog.svelte';
  import PressButton from '@ui/press-button.svelte';
</script>

let open = $state(false);
</script>

<PressButton onclick={() => (open = true)}>Open dialog</PressButton>

<Dialog title="Deploy queued" bind:open>
  <p>build #128 is waiting for a runner. The log streams once it picks up.</p>
</Dialog>

a11y

Accessibility

The native dialog element carries the modal contract — role and Escape are the platform's, fully; the platform focus trap holds for the in-dialog cycle but LEAKS every second Tab (measured, per-press receipts by two reviewers: in-dialog tabbable → the page skip link → in-dialog tabbable — the only page focusable that escapes the modal inertness).

Keyboard

KeyAction
TabCycles inside the dialog — the showModal() focus trap, WITH A MEASURED LEAK: every second press reaches the page's skip link (the one page focusable the modal inertness does not cover; reproduced per-press, two independent runs — the repair is the family's, W-next). From the skip link the next press returns to the dialog — the rest of the page IS inert
EscapeCancel event, intercepted only to share the animated close
Enter / SpaceActivate the focused control (× button, footer buttons, form method="dialog" submits)

ARIA

AttributeValueDescription
aria-labeltitleOn the dialog element — the header heading when given.
roledialog (native)The platform element; no ARIA roles to maintain.
aria-label"Close"On the × button.

theming

Density and tokens

The surface rides the shared motion kernel — one animated custom property drives entry, exit, and the scrim.

the trigger inherits the density scope, the surface inherits through the DOM tree — flip the canvas dock's density select (xs / sm / default / lg) to re-scope them together; the scrim reads in both stage themes the same way. The four-copy DensityDemo row is retired by that select.

TokenDefaultSource
--jx-p0 → 1 timelinecomponent
--scrimblack 32% / white 10%color
--jx-surface-in-x/y0px / 6pxcomponent
surface widthmin(92vw, 26rem)structural
close fade120ms (skipped under reduced motion)structural
--jx-text11 / 12 / 13 / 15pxdensity

axes

The eight axes on dialog

The eight-axis surface (explicit-props): size · shape · radius · density · color · theme · elevation · motion — each axis takes named steps, auto (inherit the ambient context; stamps nothing), an exact number (px · coefficient · dp · hue per axis), or query() for responsive/container-conditional values. The elevation axis on this modal is STAMPED, NOT PAINTED (measured): the level carriers stamp the top-layered root (radius proves the channel — the 20px seat anchors the concentric law for everything inside), but the shadow/rung recipe never paints on the served dialog — no box-shadow, filter, or backdrop-filter on any channel at settle, level4/level3/6dp/level-1/dark visually identical. The wiring decision (wire the shadow reader or retire the rung receipts) is the family owner's — W-next. The table classifies all eight axes against the served family; the query() seat rides the radius lane across the 48rem key.

PropertyTypeDefaultDescription
size 'small' | 'medium' | 'large' | 'auto' | number | query()'auto'SUPPLY-ONLY for the family's own paint — zero --jx-size-effective readers over ui/dialog/ (grep receipt); the carrier stamps the top-layered root (self-carried across the promotion), so composed head/footer snippet content scales while the panel's chrome stays pinned. Number unit: px.
shape 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto'CONSUMED at the platform element — dialog.css paints corner-shape: var(--jx-shape-effective, round) on .jx-dialog (the §14 alias ladder, round by default; the §2 named steps). No number lane.
radius 'small' | 'medium' | 'large' | 'auto' | number | query()'auto'CONSUMED — THE PAINTING LANE. dialog.css composes border-radius from --jx-radius-consumed (explicit lane: effective × the §14 factor; auto: the §3 concentric calc against the panel's OWN ancestors). An explicit lane makes the panel the CONCENTRIC ANCHOR — the carrier stamps --jx-radius-effective on the top-layered root, and children at radius="auto" compute max(0px, R − P) THROUGH the top layer (measured digit-exact: the 20px seat anchors 6px on its child). Number unit: px.
density '2xs' | 'xs' | 'sm' | 'default' | 'lg' | 'auto' | number | query()'auto' · no family ownNO OPINION on value (DialogDefaults leaves the slot unparameterized) — a named rung rides the ambient scope channel (the top-layered <dialog> stays a DOM descendant for cascade) and the panel stamps the resolved rung as data-density; auto / number / query lanes omit the stamp (densityRungOf's undefined arm). Number unit: coefficient.
color 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto'SUPPLY-ONLY — zero --jx-color-effective / hue readers over ui/dialog/ (grep receipt); the panel's ink rides the ambient token sheet, and variant paints the surface (solid | acrylic | auto), not a hue. Number unit: hue degrees.
theme 'light' | 'dark' | 'system' | 'auto''auto'THE ISLAND STAMP — theme="dark" stamps class:dark on the dialog root: a self-carried dark island THROUGH the top-layer promotion (the <dialog> stays a DOM descendant, so the island re-scopes every token read in the subtree — the card kernel's ground and ink re-derive inside it). The family itself carries zero var-chain theme reads (grep receipt). No number lane.
elevation 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | query()'level4' · Own default, not ambientOWN level4 (the modal's historic 8dp z-feel), STAMPED, NOT PAINTED (measured — W-next #18): elevationSurfaceOf composes the level-table pair onto the root style, but no box-shadow / filter / background-image channel paints on the served dialog at settle — level4 / level3 / 6dp / level-1 / dark are visually identical (the seats below teach this measured truth). The number lane is real: exact dp snaps DOWN to the enclosing table rung (6dp IS level3). The wiring decision (wire the shadow reader or retire the rung receipts) is the family owner's.
motion 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto'THE FAMILY OWNS THE MOTION, AXIS UNREAD — one 460ms --jx-p WAAPI kernel drives entry, exit, and the scrim (blurIn/slide/materials/shadow; the @starting-style pull-apart; the jx-waapi allow-discrete exit window holds the panel through the whole run), instant under prefers-reduced-motion. Zero --jx-motion-effective readers (grep receipt). Number unit: coefficient.

Receipts: the PORTAL LAW — the top-layer promotion moves PAINT, not DOM, and the axis carriers stamp the panel's OWN root either way, so the resolved axes are SELF-CARRIED (a trigger ancestor's stamps never span the boundary); the radius channel is the one elevation-adjacent lane that PAINTS (the 20px seat → 6px auto child, measured digit-exact through the top layer); the elevation rungs are the stamped-not-painted class (W-next #18, the walk found zero shadow channels at settle); the theme axis is the island stamp (class:dark on the dialog root re-scopes the card kernel's tokens inside the subtree — the family itself carries zero var-chain theme reads, grep receipt); and LAW #19 held — duplicate ids NONE page-wide (measured on the served page). The query() seat below rides the md viewport key (48rem) on the radius lane: below it the anchor is 8px (the auto child clamps at max(0px, 8 − 14) = 0px), at md+ the anchor is 20px and the child steps to 6px — the concentric law moving WITH the query.

one real query() case
<script lang="ts">
  import Dialog from '@ui/dialog.svelte';
  import PressButton from '@ui/press-button.svelte';
  import { query } from '@lib/universal-props-query.svelte';
</script>

const responsiveRadius = query({ md: 20 }, 8);
</script>

<Dialog title="Responsive radius" radius={responsiveRadius} bind:open>
  <PressButton radius="auto">auto radius rides the anchor</PressButton>
</Dialog>

Dialog · universal props

level4 · the modal default

The own level stamps level4 — the 8dp recipe and the surface-container-high rung are the level TABLE's pairing; on the served surface the shadow does not paint (measured — W-next owns the wiring).

level3 · one rung down

The level3 stamp pairs 6dp + the surface-container rung in the table — same unpainted truth as level4.

6dp · the number lane

Exact dp snaps down to the enclosing table rung — 6dp IS level3 (the mapping is real; the shadow it names is the unpainted half).

level-1 · the concave

The concave rung — the inset 1px recipe over the deepest ladder step; stamped, and like every sibling here the shadow stays unpainted (可填充的凹陷, measured).

radius 20 · the concentric anchor

children at radius="auto" compute max(0px, 20 − 14) = 6px — the §3 law through the top layer, the one elevation-adjacent lane that PAINTS (measured digit-exact).

dark · the theme axis

The dark theme axis — the table's rung STEPPING claim is unpainted on the served surface (same W-next decision); the axis itself still stamps the scope.

Responsive radius · query()

Resize across 48rem and reopen: below the key the anchor is 8px, at md+ it is 20px — and the child at radius="auto" rides the concentric calc (0px below, 6px at md+).

dialog-universal.svelte svelte
<!-- the §7 elevation × surface-ladder ladder (light, shadows lead) -->
<Dialog title="level4 · the modal default" bind:open={u1}>…</Dialog>
<Dialog title="level3 · one rung down" elevation="level3" bind:open={u2}>…</Dialog>
<!-- the number lane: exact dp snaps DOWN to the enclosing rung -->
<Dialog title="6dp · the number lane" elevation={6} bind:open={u3}>…</Dialog>
<!-- the concave rung: inset shadow + the deepest surface -->
<Dialog title="level-1 · the concave" elevation="level-1" bind:open={u4}>…</Dialog>
<!-- the concentric anchor: radius 20 supplies --jx-radius-effective
     through the top layer; children at auto compute max(0px, R − P) -->
<Dialog title="radius 20 · the anchor" radius={20} bind:open={u5}>
  <PressButton radius="auto">auto radius — 6px</PressButton>
</Dialog>

api

API

Nine props — the platform owns every behavior; the component owns state binding, zone presence, and the zone variant scopes. The footer snippet is the RAW full override of the foot zone; the head/footer content faces are the composition components below.

Properties

PropertyTypeDefaultDescription
title string—Heading of the default title row (rendered through CardHeader); omit for a chrome-less body. Still names the dialog (aria-label) when a head snippet replaces the visible row.
open bindbooleanfalseBindable open state: true → showModal(), false → animated close.
variant 'solid' | 'acrylic' | 'auto''auto' · Own default, not ambientFloating-surface paint; auto defers to the environment’s transparency preference. Defaults: literal slot — own ’auto’, ambient when an axis opens.
class string''Geometry-only utilities appended after the law’s own (a consumer’s anchor/width, a scroll-ring cap); the platform still paints nothing.
scroll booleantrueThe body zone’s scroll authority (the panel never scrolls). false asserts the body fits — the scroll authority and the stable both-edges gutter reservation retire together.
head Snippet—Replaces the visible title row — typically a CardHeader wrapping custom content; the × close still rides the head grid’s end slot.
children * Snippet—Dialog body — the only scrollable zone.
footer Snippet—The RAW full override of the foot zone — its standard content is a CardFooter (buttons auto-joined in one end-packed group, ghost by the zone’s scope).
cancelGuard () => boolean—Consulted on the native cancel request (Escape); returning true holds the dialog open (e.g. through an IME composition).

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)

api

CardHeader · CardFooter — the zone content faces

The slot architecture belongs to the zones' content, carried by components (r14-9): Dialog renders the zones and writes the ghost variant scopes; these two are what the zones usually show. CardHeader is also Dialog's internal default — the untitled title row has exactly one source.

Properties

PropertyTypeDefaultDescription
CardHeader · title string—The default title row (padded chrome bar); yields to children.
CardHeader · children Snippet—Custom head content, FLUSH edge-to-edge — owns its own geometry (the palette’s Input).
CardFooter · children Snippet—The action buttons — auto-joined in ONE ButtonGroup packed at inline-end; ghost inherited from the Dialog zone scope, an explicit variant wins; ghost seams rule the buttons.
CardFooter · end Snippet—Raw inline-end content: present, it replaces the grouped arrangement entirely — the opt-out for non-button content or a custom cluster, bracket and all.
CardFooter · opening line structuralleadingSeamThe actions region’s boundary — the ButtonGroup’s leadingSeam capability: the first button’s own flush seam pseudo (r14-13), not a sibling element; gone with the group under the end face.
CardFooter · label string'Dialog footer'The ButtonGroup’s accessible name.
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h