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

registry:ui + registry:lib

toast — two seams, no singleton

The store owns state and lifecycle (created by YOUR app, never a module singleton — SSR requests never share state); the viewport owns presentation — a GRID stack that ADOPTS into the website-scaffold's float plane when one is present (the float-button law: it never floats itself; the fixed corner is the standalone fallback), max visible with older queued, per-item live regions — role=status polite / role=alert assertive, and the unified hold (hover/focus freezes BOTH clocks — the store timer and the countdown companion), duration 0 is sticky.

install — toast

$npx jixoai-ui add toast

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

createToastStore() adopts the float plane material: popover | glass effect: pulse | sweep countdown companion pause on hover/focus sticky (duration 0) status | alert collapse → hover expand swipe to dismiss freezes when hidden expandable → dialog (VT)

overview

Overview

Two seams: a DOM-free store whose per-toast clock answers to three orthogonal freeze sources, and a viewport that adopts the scaffold's float plane — the stack dialect, per-item live regions, and the self-carried axis carriers.

The store owns state and lifecycle; the viewport owns presentation. A push queues a card into a GRID stack where every card rides the SAME cell — depth is transform vars, never overlaid positions. The clock model: one expiry timer per toast with THREE orthogonal freeze sources — the hover/focus hold, the hidden-tab pauseAll, and the queued-never-seen visibility handshake — that never stomp each other; expiry arms at FIRST VISIBILITY, so a toast beyond maxVisible never expires unseen.

Announcements are per-item: every card is its OWN live region — role=status polite, role=alert when assertive — never one region announcing everything, and the +N queued chip is aria-hidden decoration. Delivery shape: the viewport is a SELF-CARRIED PORTAL — it adopts the scaffold's float plane (the jx-top-layer context), and because a trigger ancestor's CSS carriers never span a portal adoption, the viewport stamps its OWN carriers on the stack root (the density rung, the theme island, the elevation pair). Timed surfaces measured: the auto-dismiss window, the 220ms exit-snapshot frame (reduced motion removes immediately), the 200ms/180ms enter/exit pair, and the countdown drain with its paused play-state.

The keys: a monotonic per-store counter (id: number, nextId++) — duplicate keys structurally impossible, the LAW #18 worst case (a constant burst) hydration-safe by construction; the expandable dialog's shared-element name jx-toast-<id> inherits the uniqueness. The eight axes: elevation is OWN level3 and CONSUMED (the card's ground and press shadow read the level pair through the popover fallback), density is ambient-consumed through the kernel lanes, theme splits the card's ink system into a :root-frozen ground/foreground and a scope-re-derived hue pair, and size/shape/radius/color/motion carry the supply chain (zero effective readers, grep receipt). Kinship: float-button (the material and layers model the card rides), system-dialog (the modal interruption toast refuses — no scrim, the page stays reachable), combobox (the other per-item announcement law).

usage

Usage

Create the store in app state (never a module singleton), mount the viewport once in the root layout, push from anywhere through the api handle.

Toast usage
<script lang="ts">
  import ToastViewport from '@ui/toast-viewport.svelte';
  import { createToastStore } from '@lib/toast-store';
</script>

// app state — created once, never a module singleton (SSR stays clean)
const toast = createToastStore();
let lastDeploy = $state('');
</script>

<ToastViewport store={toast} />              <!-- once, root layout -->

<PressButton onclick={() => {
  lastDeploy = '4f2a';
  toast.api.push({ title: 'Deployed', description: 'build 4f2a' });
}}>deploy</PressButton>

<PressButton onclick={() =>
  toast.api.push({ title: 'Build failed', variant: 'tonal', class: 'jx-hue-error', assertive: true, duration: 0 })
}>break it (sticky)</PressButton>

toast

Push a polite toast, a sticky assertive one, or a burst — hover a toast to freeze its countdown, drag the front card toward an edge to swipe it away, click an expandable card to read it as a dialog; the × dismisses. Older toasts queue past the visible four, and the viewport says so with the +N queued tail chip.

the store is deliberately DOM-free — anything (a worker, a server event bridge) can push through the api handle. The viewport is the only DOM half, mounted once.

toast-usage.svelte svelte
<ToastViewport store={toast} />

types

Toast variants

The ladder drives border + ink over the floating popover ground; hue is injected through the class seam (a failed status injects --error — never the destructive action hue); assertive switches the live-region role; duration 0 is sticky.

toast · variants

toast-types-demo.svelte svelte
<script lang="ts">
  import PressButton from '@ui/press-button.svelte';
  import ToastViewport from '@ui/toast-viewport.svelte';
  import { createToastStore } from '@lib/toast-store';
</script>

// app state — created once, never a module singleton (SSR stays clean)
const toast = createToastStore();

<div class="grid gap-4 sm:grid-cols-3">
  <div class="border border-border p-4"><PressButton onclick={() => toast.api.push({ title: 'Deployed', description: 'outline · polite' })}>outline</PressButton></div>
  <div class="border border-border p-4"><PressButton onclick={() => toast.api.push({ title: 'Synced', description: 'tonal · brand tint · polite', variant: 'tonal' })}>tonal</PressButton></div>
  <div class="border border-border p-4"><PressButton onclick={() => toast.api.push({ title: 'Build failed', description: 'error status · assertive · sticky', variant: 'tonal', class: 'jx-hue-error', assertive: true, duration: 0 })}>error</PressButton></div>
</div>

<!-- once, root layout -->
<ToastViewport store={toast} />

toast-v2

The stack: depth, swipe, the hidden page

One grid cell holds every wrapper (grid-area 1/1) — the stack DIALECT, not overlaid positions. THE NINE SLOTS (R3): the float plane's stage cell takes nine physical positions — left-top … right-bottom via the pos prop; the DEFAULT is right-bottom (sonner's corner). THE GROWTH LAW: the pile grows AWAY from its slot's block edge — top slots descend into the content, every other slot climbs. Collapsed, each card sits one gap deeper and 5% smaller per depth; hover or touch expands to the full list while back boards keep the geometry and surrender only the ink (text-transparent — subtraction, never reflow). Swipe dismisses toward the stack's nearest screen edges; a hidden page freezes every clock — three orthogonal freeze sources, one timer; an expandable toast morphs into its reading posture through a view transition.

toast · stack

Three long clocks stack with depth; pointer enter expands (the gap-sum ladder), leave collapses — the force-expand prop pins the posture.

A carry at or past 48px — or a flick above 0.11 px/ms — dismisses along an allowed axis; the cross axis carries at 0.2 friction. Defaults come from the slot (right-bottom → right + down, toward the nearest edges; the center takes none); a push names its own.

Push, then switch tabs: pauseAll froze the running clocks; coming back resumes exactly the remaining time. Hover holds and the queued-never-seen stay untouched — three freeze sources never stomp each other.

Card and dialog share jx-toast-<id>: the platform morphs one into the other (a WAAPI rect-rise where VT is absent). The dialog is popover=auto — light dismiss collapses back; the clock stays paused while expanded.

toast-stack-demo.svelte svelte
<script lang="ts">
  import PressButton from '@ui/press-button.svelte';
  import ToastViewport from '@ui/toast-viewport.svelte';
  import { createToastStore } from '@lib/toast-store';
</script>

// app state — created once, never a module singleton (SSR stays clean)
const toast = createToastStore();

<div class="grid w-full gap-4 sm:grid-cols-2">
  <div class="border border-border p-4 grid gap-2"><PressButton onclick={() => { for (let i = 0; i < 3; i++) toast.api.push({ title: `Sync step ${i + 1}`, description: 'hover the stack — it expands to the full list', countdown: true, duration: 15000 }); }}>collapse → hover expand</PressButton><p class="text-sm text-muted-foreground">Three long clocks stack with depth; pointer enter expands (the gap-sum ladder), leave collapses — the force-expand prop pins the posture.</p></div>
  <div class="border border-border p-4 grid gap-2"><PressButton onclick={() => toast.api.push({ title: 'Draggable', description: 'drag me toward the right screen edge — or down' })}>swipe to dismiss</PressButton><p class="text-sm text-muted-foreground">A carry at or past 48px — or a flick above 0.11 px/ms — dismisses along an allowed axis; the cross axis carries at 0.2 friction. Defaults come from the slot (right-bottom → right + down, toward the nearest edges; the center takes none); a push names its own.</p></div>
  <div class="border border-border p-4 grid gap-2"><PressButton onclick={() => toast.api.push({ title: 'Switch tabs now', description: 'this clock freezes while the page is hidden', countdown: true, duration: 20000 })}>freezes when hidden</PressButton><p class="text-sm text-muted-foreground">Push, then switch tabs: pauseAll froze the running clocks; coming back resumes exactly the remaining time. Hover holds and the queued-never-seen stay untouched — three freeze sources never stomp each other.</p></div>
  <div class="border border-border p-4 grid gap-2"><PressButton onclick={() => toast.api.push({ title: 'Incident 4f2a', description: 'click the card for the full reading posture', countdown: true, duration: 15000, expandable: true })}>expandable → dialog</PressButton><p class="text-sm text-muted-foreground">Card and dialog share jx-toast-&lt;id&gt;: the platform morphs one into the other (a WAAPI rect-rise where VT is absent). The dialog is popover=auto — light dismiss collapses back; the clock stays paused while expanded.</p></div>
</div>

<!-- once, root layout -->
<ToastViewport store={toast} />

theming

Density and tokens

The stack is floating chrome — it ADOPTS the scaffold's float plane (the fixed corner is only the standalone fallback); the demo trigger follows the density scope while the card paints through theme colors and the tone law.

xs
sm
default
lg
TokenDefaultSource
--popover / --popover-foregroundtheme colorscolor
--jx-tonal / --jx-outlineprimary / bordercolor
jx-toast-in / jx-toast-out200ms / 180mscomponent
EXIT_MS220msstructural
maxVisible4structural
store.setVisibleviewport-onlystructural
posright-bottomstructural
expand / gapfalse / 8pxstructural
SWIPE_BY_FLOAT_POSright-bottom: right, downstructural
48px / 0.11 px·ms / 0.2toast-swipe constantsstructural
store.pauseAll / resumeAllpage-visibilitystructural

api

API

The viewport is presentation-only; the store handle (api.push / api.dismiss / api.snapshot, subscribe, pause, resume) is the other seam.

Properties

PropertyTypeDefaultDescription
store * ToastStore—The app-created store (createToastStore()) — never a module singleton.
maxVisible number4Max toasts rendered at once; older ones stay queued behind the +N queued chip. 0 renders none (everything queues — never a wider render).
pos FloatPos—The float slot’s nine-grid position (left-top … right-bottom). Default right-bottom; the pile grows AWAY from the slot’s block edge and swipes toward its nearest screen edges.
expand booleanfalsePins the expanded posture — the full list, every card at its own height. Hover/touch still lifts into it while inside.
gap number8The stack’s rung spacing in px — the collapsed depth stairs and the expanded ladder both space by it.
swipeDirections readonly SwipeDirection[]—Default swipe directions when a push names none — resolved per SLOT toward its nearest screen edges (right-bottom → right + down; the center takes none).
class string''Extra classes on the corner stack.

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.promise(task, messages) — messages: pending / success / error

PropertyTypeDefaultDescription
pending * string | Omit<ToastInit, 'id'>—The in-flight notice — pushed at call time, sticky by default; replaced on settle.
success string | init | (value) => …—On resolve: replaces the pending toast (polite, default expiry). Omitted → the pending toast is simply dismissed.
error string | init | (reason) => …error shapeOn rejection: tonal + jx-hue-error, assertive, sticky — a consumer object overrides field-by-field. Omitted → the default error shape still lands.

push(init) — ToastInit

PropertyTypeDefaultDescription
title * string—Primary line, uppercase nav voice.
description string—Secondary muted line.
variant 'outline' | 'tonal''outline' · Own default, not ambientLadder prominence: outline = plain notice over the material ground; tonal = 12% tinted ground + tonal ink. Own default, not ambient (toast is outside the paint zone’s frozen availability table).
material 'popover' | 'glass''popover' · Own default, not ambientThe surface MATERIAL (float-button model): popover = solid ground; glass = the shared glass law sheet's frost (data-jx-effect stamp, tuned 12px / saturate 1 / popover 55%) — the entity-law restrained ground.
effect 'none' | 'pulse' | 'sweep''none'The effect LOOP: pulse = a breathing ring (attentive); sweep = a traveling light (live/in-flight).
countdown booleanfalseRender the countdown companion as the card’s FLOOR — a full-width drain pinned to the bottom edge; the unified hold freezes both clocks. Sticky toasts get none.
leading / trailing Snippet—The toast-grid lane slots: an icon leading, actions or a custom companion trailing.
class string—Consumer classes — the hue-injection seam, e.g. jx-hue-error.
duration number5000ms until auto-dismiss; 0 = sticky.
assertive booleanfalseThe viewport announces this one as role=alert.
expandable booleanfalsetoast-v2: clicking the card expands it into a dialog through a view transition (shared element name jx-toast-<id>; a WAAPI rect-rise where VT is absent). The clock stays paused while expanded; light dismiss collapses back.
swipeDirections readonly SwipeDirection[]SWIPE_BY_FLOAT_POS[slot]toast-v2: the drag axes that may dismiss this toast (up/down/left/right). The default vocabulary comes from the stack’s slot — right-bottom takes right + down (toward the nearest edges); the center takes none.

axes

The eight axes on toast

Elevation is OWN level3 and CONSUMED — the card's ground and press shadow read the level pair through the popover fallback (the snackbar rung); density is ambient-consumed through the kernel lanes with the rung scope stamped on the stack root; theme splits the card's ink system into a :root-frozen ground/foreground and a scope-re-derived hue pair — the carriers portal WITH the stack; size/shape/radius/color/motion carry the supply chain.

PropertyTypeDefaultDescription
density '2xs' | 'xs' | 'sm' | 'default' | 'lg' | 'auto' | number (+ the five legacy spellings)'auto'OWN NO-OPINION, AMBIENT-CONSUMED — the card's type rhythm rides the kernel lanes (--jx-text on the description, the leading rung on the dialog panel, --jx-hit/--jx-inset on the panel actions); a named rung stamps data-density ON THE STACK ROOT (the scope portals with the stack — measured lg/sm seats), a number or query lane rides the --jx-density-coefficient carrier through the kernel calc chain. Number unit: coefficient.
size 'small' | 'medium' | 'large' | 'auto' | number'auto'SUPPLY-ONLY for the family's own paint — zero --jx-size-effective readers over ui/toast/ (grep receipt); the carrier's font-size lands on the stack root, so composed leading/trailing snippet content scales while the card's own type stays pinned to the label/text tokens. Number unit: px.
shape 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto'SUPPLY-ONLY — zero --jx-shape-effective readers (grep receipt); the card corner is the static --jx-radius token. Number unit: none.
radius 'small' | 'medium' | 'large' | 'auto' | number'auto'SUPPLY-ONLY — the concentric broadcast for the reading panel: zero --jx-radius-effective readers over ui/toast/ (grep receipt); the card corner is tokens --jx-radius, not the axis calc. Number unit: px.
color 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto'SUPPLY-ONLY axis — zero --jx-color-effective readers (grep receipt); the card's hue rides the CLASS SEAM instead (jx-hue-error retints --jx-tonal/--jx-outline through the variant ladder), per the API table. Number unit: hue degrees.
theme 'light' | 'dark' | 'system' | 'auto''auto'THE SPLIT INK SYSTEM (measured, probe): the card's ground and foreground ride --jx-popover/--jx-popover-foreground — :root-pinned aliases (shape #2, substitution at the declaring element), so a scoped island holds them and ROOT-level dark re-derives; the variant hue pair --jx-tonal/--jx-outline is RE-DECLARED per theme scope (the :root, .jx-light, .dark selector list — substitution re-runs against each scope's --primary/--border), and the elevation SURFACE rung re-derives too, so a dark island flips the card's ground while the popover aliases hold (the measured split). The carrier is the .dark CLASS, never a style var — the viewport stamps class:dark on the stack root: a self-carried dark island THROUGH the portal. No number lane.
elevation 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'level3' · Own default, not ambientOWN level3, CONSUMED — the snackbar rung: elevationSurfaceOf stamps --jx-elevation-shadow / --jx-elevation-surface / --jx-surface-solid-fill on the stack root, and the card reads the pair with the popover fallback (background: var(--jx-elevation-surface, var(--jx-popover)); the press shadow composes --jx-elevation-shadow). The number lane is exact dp snapped DOWN to the table rung. Measured: the level3 pair in the stack's inline style; level4 seat flips the rung.
motion 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto'THE FAMILY OWNS THE MOTION, AXIS UNREAD — enter 200ms (jx-toast-in, --motion-200 nav ease), exit 180ms forwards, the 220ms EXIT_MS snapshot window a dismissed card paints before unmount (reduced motion removes IMMEDIATELY and skips the snapshot), pulse 1.6s / sweep 2.4s loops, and the countdown drain (linear forwards; .paused = play-state paused; RTL reverses direction) — every channel killed under prefers-reduced-motion (toast.css). Zero --jx-motion-effective readers (grep receipt). Number unit: coefficient.

Receipts: the PORTAL LAW (the viewport adopts the scaffold's float plane through the jx-top-layer context — a trigger ancestor's CSS carriers never span the adoption, so the viewport stamps its OWN carriers on the stack root: the density rung, the .dark class, the elevation pair, the coefficient), the announcement trio (every card its own live region — role=status polite / role=alert assertive, content present AT INSERT, the +N chip aria-hidden; queued cards are NOT in the accessibility tree until they render), the TRANSITION-FRAME battery (auto-dismiss windows measured with a MutationObserver page-clock instrument across multi-run ranges; the 220ms exit-snapshot frame measured, reduced-motion removal immediate; the hover hold and the hidden-tab pauseAll freeze the same clock), the queue law (burst ×5 → four visible + "+1 queued"; dismissing the front promotes the oldest queued card — FIFO order preserved, expiry arms at first visibility), the id landscape (a monotonic per-store counter — duplicate keys structurally impossible; expandable dialogs carry unique jx-toast-<id> shared-element names) and LAW #19 (duplicate ids NONE page-wide) were measured on this page's served DOM (probe, task 44). LAW #18 note: the each keys item.id — the counter makes the LAW #18 worst case (a constant burst) hydration-safe by construction. The query() seat below rides the md viewport key (48rem) on the density lane; the dark-island seat renders the measured theme split live.

one real query() case
<script lang="ts">
  import ToastViewport from '@ui/toast-viewport.svelte';
  import { createToastStore } from '@lib/toast-store';
  import { query } from '@lib/universal-props-query.svelte';
</script>

// app state — created once, never a module singleton (SSR stays clean)
const toast = createToastStore();

// the md key is the registered VIEWPORT scale (48rem): below it the
// small rung stamps, at 48rem+ large wins — data-density flips AND
// the kernel lanes rescale the card's type
const responsiveDensity = query<{ md: DensityLane }, DensityLane>({ md: 'large' }, 'small');

<ToastViewport store={toast} density={responsiveDensity} pos="left-top" />

toast · universal props

The left-bottom stack carries theme="dark" density="large" elevation=6dp: its own .dark island rides the portal — the card's ground re-derives in the island (the level3 surface rung), the tonal hue re-derives (the selector-list re-declaration), while the frozen popover aliases hold the root's values (the measured split).

The left-top stack rides the query seat: below 48rem the small rung stamps (data-density="sm"), at 48rem+ large wins ("lg") — and the card's kernel-lane type rescales with it (the description is one var(--jx-text) reader).

toast-universal.svelte svelte
<ToastViewport store={store} theme="dark" density="large" elevation={6} pos="left-bottom" />

a11y

Accessibility

Every toast is its own live region — polite by default, assertive by opt-in — never one region announcing everything. Toasts beyond maxVisible stay queued and are NOT in the accessibility tree until they render; their expiry arms at first visibility, so a queued toast never expires unseen.

Keyboard

KeyAction
TabReach the dismiss button; hover or focus on a toast pauses its countdown

ARIA

AttributeValueDescription
rolestatus | alertPer-item live region; assertive toasts announce as role=alert.
role (stack)groupThe stack container — a named group (aria-label without a role is ignored on a generic div).
aria-label (stack)notificationsNames the stack group (adopted float plane or the fixed-corner fallback).
aria-label (dismiss)dismiss notificationNames each toast’s × button.
role (expanded)dialogThe expandable reading posture (toast-v2) — popover=auto, labeled by the toast title; light dismiss collapses back to the card (the toast survives).
aria-hiddentrueThe +N queued chip and the countdown gauge are decoration — a queued toast announces when it renders, not before.
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h