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.
$npx jixoai-ui add cascader
https://ui.jixoai.com/r/cascader.json
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.
<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.
<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
| Property | Type | Default | Description |
|---|---|---|---|
| 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 bind | string[] | [] | 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 | boolean | false | Blocks 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
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity 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.
| Property | Type | Default | Description |
|---|---|---|---|
| density | data-density rung · --jx-density-coefficient | auto | Scope-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-size | auto | Stamps 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-effective | auto | Both carriers stamp on the root; no cascader css reads them. Steps round · scoop · bevel · notch · square · squircle; no number lane. |
| radius | --jx-radius-effective | auto | The 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-effective | auto | The 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 bridge | auto | A 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-effective | auto | The 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-effective | auto | The 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.
| Token | Default |
|---|---|
| --jx-unit | 0.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-10 | calc(var(--jx-unit) × 1.5 / × 2.5) — the group column gap, the chain's wrapping gap, and the select's inline padding |
| --jx-text-base | 0.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-10 | 0.75rem (12px) / 0.1em — the label's uppercase eyebrow voice |
| --jx-hairline | 1px — the select border weight, on the border role |
| --jx-radius | var(--radius) — the select corner: the theme token, not the radius axis carrier |
<!-- 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.
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.
<!-- 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
| Key | Action |
|---|---|
| Tab | Moves focus through the chain, level by level |
| ↑ / ↓ | Move within the focused select’s options (native) |
| Enter / Space | Open the focused select and commit a pick (native) |
ARIA
| Attribute | Value | Description |
|---|---|---|
| role | group | On the root; aria-label defaults to the label prop (“cascade” if omitted). |
| aria-label | level n | Per select — each level is independently named. |
| aria-labelledby | {id}-label | The 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).