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

registry:ui · General

cascader — a chain of selects, natively

The cascade selector the ruled way: N plain select elements, each listing the children of the previous pick — native keyboard, native mobile pickers, zero custom panels to keep honest. The joined leaf path submits through the bridge; a partial path submits empty.

install — cascader

$npx jixoai-ui add cascader

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

chain of native selects path value · partial submits empty native keyboard + mobile pickers zero custom panels

overview

Overview

A hidden form bridge, a labelled group, and a chain of native selects that grows one level at a time — the value is the picked path.

Cascader is the cascade selector as a chain-of-selects: level 0 lists options, each further level lists the children of the previous pick, and the chain stops growing at the first leaf-less pick. The parts are one hidden jx-form-field bridge (a joined path is not a single native control's value, so the payload rides the bridge), a labelled role="group" root, and the wrapping chain of native <select> elements.

The value law is the honest-path law: a complete path — every level picked and the last pick a leaf — joins with separator (default '/') and submits under name; a partial path submits '', never a half-truth in FormData (the input-otp law). Picking at level N truncates everything deeper, then appends. disabled blocks the whole chain; an option may disable itself at any level; form reset clears the path.

All eight universal axes resolve on the root and broadcast downward — the shells' fixed paint reads none of the carriers directly (the axis table below names each one's honesty). For the axis grammar itself, see the universal props concept page.

usage

Usage

Feed the tree; bind the path; picking a parent grows the chain, re-picking truncates deeper levels.

Cascader usage
<script lang="ts">
  import Cascader from '@ui/cascader.svelte';
</script>

const options = [
  { value: 'asia', label: 'Asia', children: [
    { value: 'japan', label: 'Japan' },
    { value: 'korea', label: 'Korea' },
  ] },
];

<Cascader {options} label="region" bind:value />

cascader

Pick Asia, then Japan — the chain grows one select at a time; re-picking an earlier level truncates the deeper ones. The echo footer shows the joined path.

region
cascader-usage.svelte svelte
<Cascader
  label="region"
  options={options}
  bind:value={[]}
/>

api

Props

The table renders from the GENERATED meta; the eight universal axis rows split into the shared section beneath the family rows. The value is the picked path; the submission is the joined string.

Properties

PropertyTypeDefaultDescription
options * CascaderOption[]—The option tree: { value, label, disabled?, children? }. Level 0 renders options; each further level renders the children of the previous pick — the chain stops growing at the first leaf-less pick.
name string—Form field name — the joined path submits under it through the jx-form-field bridge (a path is not a single native control's value).
value bindstring[][]The selected path as picked values, e.g. ['asia', 'japan'] (bind:value). Picking at level N truncates everything deeper, then appends; form reset clears it.
separator string'/'The join for the submitted string — the complete leaf path only (default 'asia/japan'). A partial path submits '', never a half-truth.
disabled booleanfalseBlocks the whole chain; form/fieldset disable propagates through the bridge (jx-disabled) and dims every select.
label string—Reads above the chain as the uppercase eyebrow; names the group (aria-label, falling back to "cascade") and labels the chain (aria-labelledby).
placeholder string'select…'The disabled first option of a not-yet-picked level — level 0 before any pick, and each deeper level as the chain grows.
class string''Forwarded to the root group element, after the family's own atoms.

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 cascader

Every axis carries a first-time no-own slot (the census batch A wiring) and resolves explicit ?? ambient ?? 'auto' on the root — an explicit lane stamps its carrier and joins the broadcast protocol (吃也供, supply-and-consume). None of the eight repaints the shells — the chain's paint is fixed constants (the token table below) — except theme's one consumed voice: the :focus outline rides var(--ring) and follows a dark island. The per-axis rows record each stamp-and-supply honestly instead of inventing paint.

Reading the table: Property is the axis, Type is the real carrier it stamps on the root here, Default is the lane default — the named steps, number unit, and consumption on this family are in each description.

PropertyTypeDefaultDescription
density data-density rung · --jx-density-coefficientautoScope-only here. Named steps resolve through the frozen alias table (small→sm · medium→default · large→lg; the five legacy spellings xs · 2xs · sm · default · lg pass verbatim) and stamp the rung on the root; a number is a coefficient re-scaling the kernel channels while the rung stays ambient. Nothing on this family repaints: the chain gap (--space-6), the select rhythm (calc(var(--jx-unit) × 1.75) block padding, --space-10 inline, 13px --jx-text-base) and the 12px label are fixed constants no rung re-bases. The rung re-bases the kernel channels for content composed INSIDE the root — a bare chain reads none of them.
size --jx-size-effective → root font-sizeautoStamps the carrier and an inline font-size on the root, but nothing here inherits it: the label reads --text-label-lg (12px) and each select reads --jx-text-base (the ruler's fixed 13px body voice), so size={24} moves the stamp, not the paint (measured below). Supply-only: nested component consumers read the carrier through the broadcast protocol. Steps small · medium · large; a number is px.
shape --jx-shape-effective · --jx-radius-factor-effectiveautoBoth carriers stamp on the root; no cascader css reads them. Steps round · scoop · bevel · notch · square · squircle; no number lane.
radius --jx-radius-effectiveautoThe carrier stamps, but the select corner reads --jx-radius — the theme token (var(--radius)) — not the effective carrier, so an explicit lane leaves the corners at the site radius. Supply-only: the concentric consumers are elsewhere (card, press-button). Steps small · medium · large; a number is px.
color --jx-color-effectiveautoThe carrier stamps; the shell ink reads the semantic core directly (--jx-border, --jx-background, --jx-foreground) and nothing consumes the hue lane on this family. Steps primary · secondary · error · warn · success · info; a number is hue degrees; a raw string passes through verbatim.
theme the .dark class bridgeautoA resolved dark stamps .dark on the root and the island re-declares the raw token layer. The one family paint reading a raw token directly is the :focus outline — var(--ring), dark's hue-drifted primary (−4°) — so the focused ring follows the island while the shell face (background, foreground, border through the :root-anchored --jx-* aliases) stays site-themed. Partial re-theme, measured: ring flips, face doesn't. Steps light · dark · system; auto inherits the tree.
elevation --jx-elevation-effectiveautoThe carrier stamps; the family paints no shadow — a chain of native selects sits in the page plane. Steps level-1 · level0 · level1 · level2 · level3 · level4 · level5; a number is exact dp.
motion --jx-motion-effectiveautoThe carrier stamps; the family runs no transitions — the disabled dim (opacity 0.5) snaps instantly, and a native popup list is platform UI. Steps reduced · subtle · normal · expressive; a number is a coefficient.

Deviations, cited: the eight-axis adoption itself is the census row — cascader is one of the 16 native-collision families of batch A (LANDED 6bb88ae0), which gave it its first-ever CascaderDefaults.resolve wiring: all eight lanes declare the no-own slot contract, and the root stamps carriers for every resolved lane (openspec/changes/explicit-props/research/migration-census.md). The §1 native-collision rule: the family destructures all eight axis names, so the native <select> elements never receive them as attributes — HTML's own select size attribute (multiple-rows count) has no passthrough to collide with; the family renders no multi-select and spreads no rest. No §13 renames apply — options, value, separator, label are family props, not axis names — and no family-local prop shadows an axis name, so the generated table needs no extra lane.

TokenDefault
--jx-unit0.25rem (4px) — the ruler unit; the select's block padding is calc(var(--jx-unit) × 1.75), fixed at every density rung
--space-6 / --space-10calc(var(--jx-unit) × 1.5 / × 2.5) — the group column gap, the chain's wrapping gap, and the select's inline padding
--jx-text-base0.8125rem (13px) — the select's mono body voice: the ruler's T_base constant, NOT the density channel --jx-text; no rung rescales it
--text-label-lg / --track-100.75rem (12px) / 0.1em — the label's uppercase eyebrow voice
--jx-hairline1px — the select border weight, on the border role
--jx-radiusvar(--radius) — the select corner: the theme token, not the radius axis carrier
the eight axes on cascader
<!-- every lane resolves on the root and broadcasts downward;
     the shells' paint is fixed — the table names each axis's honesty -->
<Cascader {options} label="ambient chain" />
<Cascader {options} label="compact chain" density="small" />
<Cascader {options} label="px size stamp" size={24} />

<!-- theme: .dark stamps the root; focus a select — the ring follows
     dark's primary, the shell face stays site-themed -->
<Cascader {options} label="dark island" theme="dark" />

<!-- query(): SSR paints the base rung (data-density="sm"); at the lg
     viewport rung (≥64rem) the root's rung attribute steps to lg.
     The explicit generics pin the case AND the base to the lane (the
     bare form infers QueryResult<string> — a type error). -->
<Cascader {options} label="responsive chain" density={query<{ lg: DensityLane }, DensityLane>({ lg: 'large' }, 'small')} />

cascader · the eight axes

The stamps, live. The shells' paint is fixed, so the demos read the root's carriers — each caption names what the SSR markup greps.

auto — ambient scope, stamps nothing (no data-density, no style attr)
ambient chain
density="small" — data-density="sm" + --jx-density-coefficient: 1 on the root; the shells' paint is fixed
compact chain
size=24 — the carrier lands in the root's style attr; the select face stays 13px
px size stamp
theme="dark" — .dark stamps the root; focus a select: the ring re-inks to dark's primary, the face stays site-themed
dark island
density={query<{ lg: DensityLane }, DensityLane>({ lg: 'large' }, 'small')}
responsive chain

SSR paints the base rung (data-density="sm"); at the lg viewport rung (≥64rem) the root's attribute steps to "lg" — a DOM fact, not a pixel fact: the shells keep their fixed paint. Resize across 64rem and watch the root.

cascader-axes.svelte svelte
<!-- every lane resolves on the root and broadcasts downward;
     the shells' paint is fixed — the table names each axis's honesty -->
<Cascader {options} label="ambient chain" />
<Cascader {options} label="compact chain" density="small" />
<Cascader {options} label="px size stamp" size={24} />

<!-- theme: .dark stamps the root; focus a select — the ring follows
     dark's primary, the shell face stays site-themed -->
<Cascader {options} label="dark island" theme="dark" />

<!-- query(): SSR paints the base rung (data-density="sm"); at the lg
     viewport rung (≥64rem) the root's rung attribute steps to lg.
     The explicit generics pin the case AND the base to the lane (the
     bare form infers QueryResult<string> — a type error). -->
<Cascader {options} label="responsive chain" density={query<{ lg: DensityLane }, DensityLane>({ lg: 'large' }, 'small')} />

a11y

Accessibility

Every level is a real native select — the platform's keyboard and mobile pickers come free.

Keyboard

KeyAction
TabMoves focus through the chain, level by level
↑ / ↓Move within the focused select’s options (native)
Enter / SpaceOpen the focused select and commit a pick (native)

ARIA

AttributeValueDescription
rolegroupOn the root; aria-label defaults to the label prop (“cascade” if omitted).
aria-labellevel nPer select — each level is independently named.
aria-labelledby{id}-labelThe chain is labelled by the visible label when present.

The chain's hit surface is density-invariant: the native select measures 35px tall at every rung — the shell rhythm is fixed — clearing the 24px WCAG 2.5.8 AA target floor (measured ambient, sm and lg). The focus law is the family's own: a 1px inset ring on the focused select, unlayered so it always paints; its ink follows the theme island when one resolves (the axis table's theme row).

jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h