Cascader
ProMulti-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.
import { SvCascaderComponent } from '@styloviz-pro/forms';Demo & usage
Basic usage
ProUsage example — open the gallery for the full interactive demo.
An interactive demo for this component lives in the gallery.
Open live demoMore examples
Hover expand + partial select
ProAsync lazy-loaded children
ProGive branch nodes isLeaf:false and no children; loadData fetches them on expand (a spinner shows meanwhile).
Multi-select with removable chips (Pro)
ProSet 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
| Name | Type | Default | Description |
|---|---|---|---|
| options | CascaderNode[] | [] | Tree of selectable nodes. |
| loadData | ((node: CascaderNode, signal?: AbortSignal) => CascaderNode[] | Promise<CascaderNode[]>) | null | null | Async 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. |
| value | string[] | [] | Selected value path as array of values, e.g. ['country', 'state', 'city']. |
| placeholder | string | 'Select...' | Placeholder text. |
| changeOnSelect | boolean | false | Allow selecting intermediate (non-leaf) nodes. |
| searchable | boolean | false | Show search input in dropdown. |
| disabled | boolean | false | Disabled state. |
| expandTrigger | CascaderExpandTrigger | 'click' | Expand trigger. |
| columnMode | CascaderColumns | 'auto' | Side-by-side columns, one column at a time, or whichever fits. |
| separator | string | ' / ' | Path separator shown in the trigger. |
| size | CascaderSize | 'md' | Component size. |
| multiple | boolean | false | Multi-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`. |
| clearable | boolean | true | Show the clear button while there is a selection. |
| checkedValues | string[][] | [] | Initial checked value paths for multi-select mode. |
| serverSide | boolean | false | Let the server do the searching. The supplied `options` are shown exactly as given — the flat search list is built from them without re-filtering. |
| debounceMs | number | 300 | Delay before `queryChange` fires, in ms. `0` emits immediately. |
| minChars | number | 0 | Characters required before a search is sent. Below this the panel says so instead of showing a misleading "No results". |
| error | SvDataError | string | null | null | Failure 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. |
| stale | boolean | false | The 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. |
| ariaLabel | string | '' | Accessible name for the trigger / panel. |
| errorMessage | string | '' | Client-side validation message. |
| serverError | SvServerError | null | A rejection reported by the server about this value. Self-expiring. |
Outputs
| Event | Payload type | Description |
|---|---|---|
| valueChange | string[] | Emitted when a value is selected (single-select mode). |
| queryChange | string | The search term changed — fetch matching paths. Debounced by `debounceMs` and never emitted twice for the same term. Pair with `serverSide`. |
| loadError | CascaderNode | A branch's children failed to load. The node keeps a retry control; this lets the host log or toast as well. |
| retry | void | The host should re-run the failed root load (see `error`). |
| selectionChange | CascaderNode[] | Emitted with full node path on selection (single-select mode). |
| valuesChange | string[][] | Emitted in multi-select mode with every checked value path. |
| selectionsChange | CascaderNode[][] | 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.