Cascader

Pro

Multi-level cascading select for hierarchical data (e.g. Country → State → City) with searchable mode, hover/click expand triggers, partial-path selection, multi-select with removable chips, async lazy-loaded children and full keyboard navigation.

</>sv-cascader cascaderselecthierarchyasyncmulti-select
import { SvCascaderComponent } from '@styloviz-pro/forms';

Demo & usage

Basic usage

Pro

Usage example — open the gallery for the full interactive demo.

Pro

An interactive demo for this component lives in the gallery.

Open live demo

More examples

Hover expand + partial select

Pro

Async lazy-loaded children

Pro

Give branch nodes isLeaf:false and no children; loadData fetches them on expand (a spinner shows meanwhile).

Multi-select with removable chips (Pro)

Pro

Set multiple=true — leaf nodes become checkboxes, the panel stays open while picking, and each pick shows as a removable chip in the trigger.

When to use

  • Selecting from naturally hierarchical data: location, category trees, org units.
  • You want one compact control instead of several dependent dropdowns.
  • For a flat list use Select Dropdown; for browsing a big tree use Tree View.

Async states

  • error
  • retry

Inputs

NameTypeDefaultDescription
optionsCascaderNode[][]Tree of selectable nodes.
loadData((node: CascaderNode, signal?: AbortSignal) => CascaderNode[] | Promise<CascaderNode[]>) | nullnullAsync children loader. When provided, a node with no `children` and `isLeaf !== true` is treated as an unloaded branch: clicking it calls `loadData(node)` and attaches the resolved children.
valuestring[][]Selected value path as array of values, e.g. ['country', 'state', 'city'].
placeholderstring'Select...'Placeholder text.
changeOnSelectbooleanfalseAllow selecting intermediate (non-leaf) nodes.
searchablebooleanfalseShow search input in dropdown.
disabledbooleanfalseDisabled state.
expandTriggerCascaderExpandTrigger'click'Expand trigger.
columnModeCascaderColumns'auto'Side-by-side columns, one column at a time, or whichever fits.
separatorstring' / 'Path separator shown in the trigger.
sizeCascaderSize'md'Component size.
multiplebooleanfalseMulti-select mode (Pro). Leaf nodes render checkboxes, the panel stays open while picking, and selections surface as removable chips in the trigger. Single-select behaviour is unchanged when `false`.
clearablebooleantrueShow the clear button while there is a selection.
checkedValuesstring[][][]Initial checked value paths for multi-select mode.
serverSidebooleanfalseLet the server do the searching. The supplied `options` are shown exactly as given — the flat search list is built from them without re-filtering.
debounceMsnumber300Delay before `queryChange` fires, in ms. `0` emits immediately.
minCharsnumber0Characters required before a search is sent. Below this the panel says so instead of showing a misleading "No results".
errorSvDataError | string | nullnullFailure loading the root options, rendered in place of the columns. Pair it with `(retry)`: unlike a failed branch, which the user can re-open by clicking the node again, a failed root leaves an empty panel with nothing to press.
stalebooleanfalseThe results on screen answer an **older** query — a newer request is still in flight. Distinct from `loading`, which cannot separate the two cases it covers: the first fetch (nothing on screen — show a skeleton) and a refinement of results already visible (keep them, and say they are provisional). Blanking the panel on the second is the most jarring thing a picker can do; showing the old list with no indication is a quiet lie. Bind `svSuggestController(...).stale` from `@styloviz/data`, or `createRequestGuard().stale` from `@styloviz/core`. The results dim and report `aria-busy` while staying fully interactive.
ariaLabelstring''Accessible name for the trigger / panel.
errorMessagestring''Client-side validation message.
serverErrorSvServerErrornullA rejection reported by the server about this value. Self-expiring.

Outputs

EventPayload typeDescription
valueChangestring[]Emitted when a value is selected (single-select mode).
queryChangestringThe search term changed — fetch matching paths. Debounced by `debounceMs` and never emitted twice for the same term. Pair with `serverSide`.
loadErrorCascaderNodeA branch's children failed to load. The node keeps a retry control; this lets the host log or toast as well.
retryvoidThe host should re-run the failed root load (see `error`).
selectionChangeCascaderNode[]Emitted with full node path on selection (single-select mode).
valuesChangestring[][]Emitted in multi-select mode with every checked value path.
selectionsChangeCascaderNode[][]Emitted in multi-select mode with every checked node path.

Customization & theming

  • Feed `options` as a `CascaderNode[]` tree; the path is emitted as an array of values via `[(value)]`.
  • Set `expandTrigger` to `hover` or `click`, and allow partial paths with `changeOnSelect`.
  • Enable `searchable` for flat path search, tune `separator` for the displayed label, and pick a `size` (sm, md, lg).
  • Provide `loadData` to lazily fetch children on expand — mark branch nodes with `isLeaf: false` and no `children`; a spinner shows while loading.
  • Turn on `multiple` for multi-select: leaves render checkboxes, the panel stays open, and picks surface as removable chips. Bind `[(checkedValues)]` (a `string[][]` of value paths) and listen to `valuesChange` / `selectionsChange`.
  • Toggle the clear affordance with `clearable` (defaults to on) — it clears the single value or every checked chip.
  • Implements `ControlValueAccessor`, so single-select also binds with `[formControl]` / `[(ngModel)]` and respects `control.disable()` — `[(value)]` still works for non-form usage.
  • Responsive: the body-portaled panel caps its width and height to the viewport and the column strip scrolls horizontally, so a multi-column cascader never runs off-screen on a phone or tablet.

Best practices

  • Do: Keep the hierarchy to 2–4 levels so the path stays readable in the trigger.
  • Do: Enable `searchable` when any level has many nodes.
  • Tip: Use `changeOnSelect` only when intermediate (non-leaf) selections are valid.
  • Do: Handle a saved path whose middle node has gone. It used to render as a SHORTER, complete-looking path — `[eu, uk, lon]` with the UK branch removed read simply as "Europe", which is a selection somebody could plausibly have made — while the form still held the three-level value. It now reads `Europe / 2 unavailable`, and `hasStalePath()` / `unresolvedDepth()` / `stalePathValues()` let you react.
  • Don't: Don't assume a short path is a broken one. Selecting a non-leaf node is legitimate here: "Europe" chosen on purpose must look exactly like "Europe" chosen on purpose, and different from "Europe" as the wreck of a deeper path. That is what the unavailable count distinguishes.
  • Do: Stamp submissions with a schema version and migrate them on load. A v1 submission opened under v2 was undefined in three ways: renamed fields were SILENTLY DROPPED on the next save (`svPruneHidden` cannot tell "not in the schema" from "hidden"), surviving values kept their old TYPE until somebody touched the field, and nothing recorded which schema produced the submission. `svMigrateValue` runs your steps and reports what is left over.
  • Do: Call `svOrphanedKeys(schema, value)` before you save. It is what turns a rename from silent data loss into a reported condition — you get `["name", "dept"]` and can migrate, warn, or refuse, rather than discovering months later that a column emptied.
  • Don't: Don't write one "upgrade from anything" function. `SvSchemaMigration` steps go from `to - 1` to `to`, because a submission can be three versions behind and a function handling every origin grows a branch per release and is tested for none of them. A missing step STOPS the chain and sets `incomplete` — running v3's migration on a v1 value usually "succeeds" and quietly produces nonsense.
  • Do: Take the optional second argument in `loadData` — an `AbortSignal` — and honour it. A branch the user has navigated away from is now cancelled; before, the request kept running and its response was inserted into a column that was no longer on screen. On a deep tree browsed quickly that was dozens of live requests.

Accessibility

  • The trigger exposes `aria-expanded`/`aria-haspopup` and is named via `ariaLabel` (announced with the selected path); each column is a `role="listbox"` of `role="option"` rows with `aria-selected`.
  • The popover is a non-modal `role="dialog"`; the search box is labelled; arrows move within a level and Esc closes.
  • In multi-select mode each leaf row reflects its checked state via `aria-selected`, and every chip has a labelled "Remove <item>" control operable with Enter/Space.
  • Closes on outside click and restores focus to the trigger; all panel ids are generated per instance.

Feedback on Cascader

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.