Cascader
Accessible, unstyled Cascader: typed multi-level option paths in adjacent listbox columns.
|
|
| Package |
@vizejs/ui/cascader |
| Maturity |
stable |
| Own the source |
vize lib pull cascader |
| Requires |
context, controllable-state, dismissable-layer, id, portal, positioner, presence |
| Aliases |
cascader, cascade select, multi-level select, hierarchical picker |
| Covers |
Ant Design Cascader, Element Plus Cascader, PrimeVue CascadeSelect |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { Cascader, CascaderRoot, CascaderColumn, CascaderContent, CascaderItem, CascaderTrigger, CascaderValue } from "@vizejs/ui/cascader";
Or copy the source into your project with vize lib pull cascader (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Cascader that picks a delivery region by drilling from country to prefecture to city. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import {
Cascader,
CascaderColumn,
CascaderContent,
CascaderItem,
CascaderTrigger,
CascaderValue,
} from "@vizejs/ui/cascader";
interface Region {
readonly value: string;
readonly label: string;
readonly children?: readonly Region[];
}
const regions: readonly Region[] = [
{
value: "jp",
label: "Japan",
children: [
{
value: "tokyo",
label: "Tokyo",
children: [
{ value: "shibuya", label: "Shibuya" },
{ value: "shinjuku", label: "Shinjuku" },
],
},
{ value: "osaka", label: "Osaka", children: [{ value: "kita", label: "Kita" }] },
],
},
{
value: "fr",
label: "France",
children: [
{ value: "paris", label: "Paris" },
{ value: "lyon", label: "Lyon" },
],
},
];
const path = ref<readonly Region[]>([]);
const labelId = useId();
</script>
<template>
<div>
<span :id="labelId">Delivery region</span>
<Cascader
v-slot="{ columns }"
v-model="path"
:options="regions"
by="value"
:item-text="(region: Region) => region.label"
placeholder="Choose a region"
name="region"
>
<CascaderTrigger :aria-labelledby="labelId">
<CascaderValue />
</CascaderTrigger>
<CascaderContent>
<CascaderColumn v-for="column in columns" :key="column.level" :level="column.level">
<CascaderItem v-for="region in column.options" :key="region.value" :value="region">
{{ region.label }}
</CascaderItem>
</CascaderColumn>
</CascaderContent>
</Cascader>
</div>
</template>
API
Cascader<T, Multiple extends boolean = false>
Source: src/families/selection/cascader/cascader-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
options (required) |
readonly T[] |
required |
Top-level options. |
modelValue |
CascaderModelValue<T, Multiple> |
undefined |
Controlled selection: one path, or a list of leaf paths when multiple is true. |
defaultValue |
CascaderModelValue<T, Multiple> |
undefined |
Initial selection for uncontrolled use and the value restored by reset. |
multiple |
Multiple |
false |
Select several leaf paths. The literal type decides the model type. |
getChildren |
(node: T) => readonly T[] | undefined |
undefined |
Child accessor. Defaults to an array-valued children property. |
isLeaf |
(node: T) => boolean |
undefined |
Leaf predicate. Defaults to "has no children and nothing to load". |
loadChildren |
(node: T, context: CascaderLoadContext) => Promise<readonly T[]> |
undefined |
Lazily load a branch's children the first time it expands. |
by |
CascaderBy<T> |
undefined |
Compare nodes by a property key or with a custom equality function. |
itemText |
(node: T) => string |
undefined |
Human-readable text for a node, used by the value display, typeahead, and search. |
itemDisabled |
(node: T) => boolean |
undefined |
Disable individual nodes. |
formValue |
(node: T) => string |
undefined |
Serialize one node for form submission; path segments are joined by separator. |
changeOnSelect |
boolean |
false |
Allow choosing branch options, not only leaves (single mode). |
expandTrigger |
CascaderExpandTrigger |
"click" |
Expand branches on click or on pointer hover. |
separator |
string |
" / " |
Separator between path segments in display text and form values. |
search |
string |
undefined |
Controlled search text; a non-empty query publishes searchResults. |
open |
boolean |
undefined |
Controlled open state. undefined selects uncontrolled behavior. |
defaultOpen |
boolean |
false |
Initial open state for uncontrolled use. |
disabled |
boolean |
false |
Disable the trigger and every option. |
required |
boolean |
false |
Mark a selection as required for assistive technology. |
name |
string |
undefined |
Form field name; each selected path submits one hidden input. |
form |
string |
undefined |
Id of the owning form when rendered outside it. |
placeholder |
string |
undefined |
Text shown by CascaderValue while nothing is selected. |
typeaheadTimeout |
number |
500 |
Idle time before buffered typeahead starts a new query. |
Events
| Event |
Payload |
Description |
update:modelValue |
[value: CascaderModelValue<T, Multiple>] |
Fired when the selection requests a new controlled value (a path, or a list of paths). |
change |
[ value: CascaderModelValue<T, Multiple>, previous: CascaderModelValue<T, Multiple>, nativeEvent: Event | null, ] |
Fired after a user interaction changes the selection: next value, previous value, native event. |
update:open |
[open: boolean] |
Fired when the popup requests a controlled open value. |
update:search |
[query: string] |
Fired when the search text requests a new controlled value. |
load-error |
[node: T, error: unknown] |
Fired when loadChildren rejects for a branch, with the error. |
Slots
| Slot |
Slot props |
Description |
default |
CascaderSlotState<T> |
Trigger, value, and popup parts. Receives columns, selection, and search results. |
Exposed
| Member |
Type |
Description |
clear |
— |
|
expandPath |
— |
|
focus |
— |
|
open |
— |
|
reset |
— |
|
selected |
— |
|
selectPath |
— |
|
setOpen |
— |
|
CascaderRoot<T, Multiple extends boolean = false>
Source: src/families/selection/cascader/cascader-root.vue
Props
| Prop |
Type |
Default |
Description |
id |
string | null |
undefined |
Consumer-owned base id. null and undefined select a deterministic fallback. |
options (required) |
readonly T[] |
required |
Top-level options. |
modelValue |
CascaderModelValue<T, Multiple> |
undefined |
Controlled selection: one path, or a list of leaf paths when multiple is true. |
defaultValue |
CascaderModelValue<T, Multiple> |
undefined |
Initial selection for uncontrolled use and the value restored by reset. |
multiple |
Multiple |
false |
Select several leaf paths. The literal type decides the model type. |
getChildren |
(node: T) => readonly T[] | undefined |
undefined |
Child accessor. Defaults to an array-valued children property. |
isLeaf |
(node: T) => boolean |
undefined |
Leaf predicate. Defaults to "has no children and nothing to load". |
loadChildren |
(node: T, context: CascaderLoadContext) => Promise<readonly T[]> |
undefined |
Lazily load a branch's children the first time it expands. |
by |
CascaderBy<T> |
undefined |
Compare nodes by a property key or with a custom equality function. |
itemText |
(node: T) => string |
undefined |
Human-readable text for a node, used by the value display, typeahead, and search. |
itemDisabled |
(node: T) => boolean |
undefined |
Disable individual nodes. |
formValue |
(node: T) => string |
undefined |
Serialize one node for form submission; path segments are joined by separator. |
changeOnSelect |
boolean |
false |
Allow choosing branch options, not only leaves (single mode). |
expandTrigger |
CascaderExpandTrigger |
"click" |
Expand branches on click or on pointer hover. |
separator |
string |
" / " |
Separator between path segments in display text and form values. |
search |
string |
undefined |
Controlled search text; a non-empty query publishes searchResults. |
open |
boolean |
undefined |
Controlled open state. undefined selects uncontrolled behavior. |
defaultOpen |
boolean |
false |
Initial open state for uncontrolled use. |
disabled |
boolean |
false |
Disable the trigger and every option. |
required |
boolean |
false |
Mark a selection as required for assistive technology. |
name |
string |
undefined |
Form field name; each selected path submits one hidden input. |
form |
string |
undefined |
Id of the owning form when rendered outside it. |
placeholder |
string |
undefined |
Text shown by CascaderValue while nothing is selected. |
typeaheadTimeout |
number |
500 |
Idle time before buffered typeahead starts a new query. |
Events
| Event |
Payload |
Description |
update:modelValue |
[value: CascaderModelValue<T, Multiple>] |
Fired when the selection requests a new controlled value (a path, or a list of paths). |
change |
[ value: CascaderModelValue<T, Multiple>, previous: CascaderModelValue<T, Multiple>, nativeEvent: Event | null, ] |
Fired after a user interaction changes the selection: next value, previous value, native event. |
update:open |
[open: boolean] |
Fired when the popup requests a controlled open value. |
update:search |
[query: string] |
Fired when the search text requests a new controlled value. |
load-error |
[node: T, error: unknown] |
Fired when loadChildren rejects for a branch, with the error. |
Slots
| Slot |
Slot props |
Description |
default |
CascaderSlotState<T> |
Trigger, value, and popup parts. Receives columns, selection, and search results. |
Exposed
| Member |
Type |
Description |
selected |
readonly (readonly T[])[] |
Selected paths. |
open |
boolean |
Whether the popup is open. |
setOpen |
(open: boolean) => boolean |
Request a specific open state. |
selectPath |
(path: readonly T[]) => boolean |
Choose a path (toggle in multiple mode). |
expandPath |
(path: readonly T[]) => void |
Expand the branch at the end of path, loading it when needed. |
clear |
() => boolean |
Clear the selection. |
reset |
() => boolean |
Restore defaultValue. |
focus |
(options?: FocusOptions) => void |
Focus the trigger. |
CascaderColumn
Source: src/families/selection/cascader/cascader-column.vue
Props
| Prop |
Type |
Default |
Description |
level (required) |
number |
required |
Zero-based depth of the options this column lists. |
ariaLabel |
string |
undefined |
Accessible name; defaults to the parent option's text (or "Options" at the top level). |
Slots
| Slot |
Slot props |
Description |
default |
CascaderColumnSlotState |
CascaderItem options for this level; render them from the root columns state. |
CascaderContent
Source: src/families/selection/cascader/cascader-content.vue
Props
| Prop |
Type |
Default |
Description |
placement |
Placement |
"bottom-start" |
Preferred placement relative to the trigger before collision handling. |
strategy |
PositionerStrategy |
"fixed" |
CSS positioning mode published on the floating host. |
offset |
number |
4 |
Gap between the trigger and the popup. |
collisionPadding |
number |
8 |
Viewport padding the popup should not cross. |
flip |
boolean |
true |
Flip to the opposite side when the preferred side overflows more. |
shift |
boolean |
true |
Shift the popup back into the viewport. |
size |
boolean |
true |
Publish available-size CSS variables and constrain the host. |
to |
string | HTMLElement |
"body" |
CSS selector or element the popup is moved into. |
portalDisabled |
boolean |
false |
Render in place instead of teleporting. |
defer |
boolean |
true |
Keep content in place until the portal target exists, avoiding SSR mismatch. |
forceMount |
boolean |
false |
Keep items mounted (hidden) while closed. |
ariaLabel |
string |
undefined |
Accessible name of the popup region holding the columns. |
Events
| Event |
Payload |
Description |
escape-key-down |
[event: DismissableLayerEscapeKeyDownEvent] |
Fired before Escape closes the popup. Call preventDefault() to keep it open. |
pointer-down-outside |
[event: DismissableLayerPointerDownOutsideEvent] |
Fired before an outside pointer-down closes the popup. Call preventDefault() to keep it open. |
dismiss |
[event: DismissableLayerDismissEvent] |
Fired after an unprevented dismissal request closes the popup. |
Slots
| Slot |
Slot props |
Description |
default |
CascaderContentSlotState |
One CascaderColumn per entry of the root columns. Receives open state and placement. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
CascaderItem<T>
Source: src/families/selection/cascader/cascader-item.vue
Props
| Prop |
Type |
Default |
Description |
value (required) |
T |
required |
Node rendered by this option; must belong to the column's options. |
ariaLabel |
string |
undefined |
Accessible name when the option text is not enough. |
Slots
| Slot |
Slot props |
Description |
default |
CascaderItemSlotState<T> |
Option content. Receives branch, expansion, selection, and loading state. |
CascaderTrigger
Source: src/families/selection/cascader/cascader-trigger.vue
Props
| Prop |
Type |
Default |
Description |
ariaLabel |
string |
undefined |
Accessible name when no visible label supplies one. |
ariaLabelledby |
string |
undefined |
Space-separated ids of visible labels. |
ariaDescribedby |
string |
undefined |
Space-separated ids that describe the cascader. |
Slots
| Slot |
Slot props |
Description |
default |
{ readonly open: boolean; readonly empty: boolean } |
Trigger content, usually CascaderValue. Receives open and emptiness state. |
Exposed
| Member |
Type |
Description |
element |
Readonly<ShallowRef<HTMLElement | null>> |
Template ref to the rendered root element. |
focus |
— |
|
CascaderValue
Source: src/families/selection/cascader/cascader-value.vue
Props
| Prop |
Type |
Default |
Description |
pathSeparator |
string |
", " |
Text placed between paths when several are selected. |
Slots
| Slot |
Slot props |
Description |
default |
CascaderValueSlotState |
Custom rendering of the selected paths. |
placeholder |
CascaderValueSlotState |
Custom placeholder rendering while nothing is selected. |
Behavior
Contract
Cascader is a headless, strongly typed multi-level picker. cascader-root.vue
is generic over the node type T and a Multiple literal: the model is one
option path (readonly T[], empty when nothing is selected) or, with
multiple: true, a list of leaf paths (readonly (readonly T[])[]). Children
come from getChildren (default: an array children property) or lazily from
loadChildren(node, { signal }); nodes compare through by.
cascader-trigger.vue renders a native <button role="combobox" aria-haspopup="listbox"> that keeps DOM focus and exposes the highlighted
option through aria-activedescendant; aria-controls lists every open
column. cascader-content.vue renders the popup through Portal, Presence,
Positioner, and a dismissable layer. The root publishes typed columns (the
top level plus one per expanded branch); each cascader-column.vue is a
role="listbox" labelled by its parent option and each cascader-item.vue a
role="option" with aria-expanded on branches. cascader-value.vue renders
the selected path text or a placeholder. Named cascaders submit one hidden
input per selected path (formValue/by segments joined by separator).
Option ids derive from the root id plus level and index. No CSS ships.
Normative Behavior
| # |
State |
Input |
Outcome |
Proven by |
| K1 |
closed |
render |
combobox trigger with aria-haspopup="listbox", aria-expanded="false", placeholder text, no columns |
renders a combobox trigger with placeholder and no popup while closed |
| K2 |
open |
click branch / click leaf |
branches open child columns labelled by their parent and set aria-expanded; a leaf selects its path, closes, and shows A / B / C |
click expands branches into new columns and selecting a leaf closes |
| K3 |
selected |
reopen |
expands the selected path, highlights its last node, marks ancestors data-state="partial" |
reopening restores the selected path and marks ancestors as partial |
| K4 |
closed / open |
ArrowDown / Up / Home / End / ArrowRight / Enter / ArrowLeft / Space |
opens; moves within a column skipping disabled nodes; ArrowRight and Enter on a branch descend to its first option; ArrowLeft returns to the parent and closes deeper columns; Enter/Space on a leaf selects and closes |
keyboard matrix: open, arrows skip disabled, right/left traverse levels, Enter selects |
| K5 |
open |
printable keys / Escape |
typeahead per column (navigation keys reset the query); Escape closes without selecting |
typeahead moves within the active column and Escape closes without selecting |
| K6 |
changeOnSelect |
click branch |
commits the branch path and keeps the popup open |
changeOnSelect commits branch paths while keeping the popup open |
| K7 |
multiple |
click leaves |
toggles leaf paths, stays open, marks columns aria-multiselectable, submits one hidden input per path |
multiple mode toggles leaf paths and stays open |
| K8 |
expandTrigger=hover |
pointer move over a branch |
highlights and expands it |
hover expandTrigger expands branches on pointer movement |
| L1 |
loadChildren |
expand / expand a sibling / resolve |
loads once, marks the option data-loading and its column aria-busy, aborts a superseded sibling load, descends after a keyboard-triggered load |
loadChildren loads lazily, marks loading, descends by keyboard, and aborts superseded loads |
| L2 |
loadChildren |
reject / unmount |
emits load-error(node, error); unmount aborts pending loads |
unmount aborts pending loads and rejected loads emit load-error |
| S1 |
search |
non-empty query |
publishes matching (loaded) leaf paths as searchResults; selectPath chooses one |
search publishes matching paths that can be selected |
| C1 |
controlled / disabled |
select / keys |
controlled values emit and wait for the parent; disabled cascaders never open |
controlled values wait for the parent; disabled cascaders never open |
| C2 |
no provider |
mount a part |
throws VIZE_UI_CONTEXT_MISSING |
parts require a Cascader provider |
| U1 |
pure helpers |
findCascaderPath / flattenCascaderPaths / searchCascaderPaths / toCascaderSelection |
resolve, flatten (optionally including branches), search accent-insensitively, and normalize models |
pure helpers resolve, flatten, search, and normalize paths |
SSR
Ids derive from the deterministic-id primitive, the popup renders in place
until the portal hydrates, and lazy loads only start after mount. Closed and
open trees render byte-identical markup and hydrate without warnings
(renders byte-identical closed Cascader markup with the selected path label,
renders byte-identical open Cascader markup with columns for the selected path, hydrates closed and open Cascaders without mismatches).