CascadeSelect
Two levels, one pick.
About
Picks one leaf from a tree of options: each branch opens its own list beside the last, and on a phone each level replaces the one before under a back row. Use it for a choice like country, state, then city, where a flat list would repeat the parents. Form-associated: posts the leaf's value under its name.
Usage
Basic
The trigger is a combobox with aria-haspopup="tree" and aria-activedescendant on the active row. Every row is a treeitem with aria-level, aria-posinset and aria-setsize; a branch has aria-expanded, a leaf aria-selected, a disabled row aria-disabled. A combobox may own a tree, not a menu, so the keys are the tree ones.
Three levels
options is a tree of { label, value, disabled, icon, items } nodes to any depth. A node with items is a branch that opens the next level; one without is a leaf, and only a leaf is picked. value defaults to label and is what the form posts. Hamilton is disabled: announced, not picked. On a phone a level fills the menu.
Path in the trigger
showPath puts the branches above the pick before it, for a leaf whose label alone is ambiguous, such as a city name two states share. When the box is narrow the branches give way to an ellipsis first and the leaf keeps its words.
Open flyout
path holds the open branches, one index per level: [1] opens the second root branch. It is a public mutable prop, so hover, a click and the keyboard all move it and announce the move. Opening from the trigger opens the path to the pick.
As a property
variant="quiet" draws the picked leaf as text in the surrounding font, with no shell at rest: the fill comes on hover, the ring on focus, the shell while the menu is open. The label is not drawn but still names the control. Opened with a pick and no path, the menu opens the path to the pick.
States
An error replaces the help line, paints the trigger in the danger tone and sets aria-invalid. A disabled control posts nothing and never holds the form.
Loading, clear and sizes
While loading is set a spinner stands in for the chevron and the panel shows the loading row instead of options. clearable draws an X that clears the pick, and size is the same sm, md, lg vocabulary as the rest of the control family.
Read-only
readonly keeps the trigger named and in the tab order, opens nothing and clears nothing, and the value still posts.
Empty
emptyMessage is the row shown when there are no options, and in a branch whose items is empty. A cascade has no filter, so it carries no emptyFilterMessage.
In a form
The picked leaf posts its value under the host name through ElementInternals, so Utilities posts utilities. An empty control posts an empty string, the way a native select does.
Events
suiOpenPathChange fires after the element moves the open path itself and carries the new path, one index per level. suiChange carries the picked leaf value. suiOpen fires after it opens, and the cancelable suiClose fires before it closes with a reason of pick, escape, outside or trigger.
Keyboard
| Key | Action |
|---|---|
| ArrowDown ArrowUp | Opens the panel. Once open, moves through the rows of the current level, wrapping at either end and stepping over disabled rows. |
| ArrowRight | On a branch, opens its level and moves to its first row. |
| ArrowLeft | On an open branch, closes its level; anywhere else, moves to the branch that opened the current level. On a phone, climbs back one level. Under dir="rtl" the pair swaps, so the arrow that points along the reading direction is always the forward one. |
| Enter Space | Opens the panel, opens the active branch, or picks the active leaf. |
| Home End | Moves to the first or last row of the current level that can be picked. |
| Escape | Closes the panel and returns focus to the trigger. |
| Tab | Closes the panel and moves on. |
| Delete Backspace | On the trigger, with clearable set, clears the value. |
API
<sui-cascade-select>
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
clearable | clearable | boolean | false | X control to clear the value. Delete or Backspace on the trigger clears it from the keyboard. |
disabled | disabled | boolean | false | Disables the trigger. A disabled control posts nothing and never holds the form. |
emptyMessage | empty-message | stringstring | undefined | Line shown in the panel when there are no options, and in a branch whose items is empty. This element does not filter, so it has no emptyFilterMessage: there is only one empty state to name. | |
error | error | stringstring | undefined | Error message; paints the trigger in the danger tone and sets aria-invalid. | |
help | help | stringstring | undefined | Help text under the trigger. Replaced by error when set. | |
invalid | invalid | boolean | false | Danger border without a message, for a form that draws its summary elsewhere. |
label | label | stringstring | undefined | Label above the trigger. | |
loading | loading | boolean | false | Shows the options are still on their way: a spinner stands in for the chevron and the menu shows the loading row instead of options. Set it while fetching, clear it when options land. |
loadingMessage | loading-message | stringstring | undefined | Text of the loading row. | |
name | name | stringstring | undefined | Form control name. Reflected: the form entry is built from the content attribute. | |
open | open | boolean | false | Opens the menu. Mutable and the single source of truth: suiOpen announces an open the element performed, and the cancelable suiClose asks before it closes itself on pick, outside click, Escape and Tab. |
openPath | property only | number[] | [] | The open branches, one index per level: [] shows the root list alone, [0, 2] opens the first root branch and, beside it, that branch's third. Mutable: hover, click and the arrow keys move it, and every move the element performs itself is announced with suiOpenPathChange, so a binding never goes stale on hover. Opening from the trigger opens the path to the pick. Set it with open to show a level of your choosing. Property, not attribute. |
options | property only | SuiCascadeOption[] | [] | The tree: { label, value, disabled, icon, items } nodes to any depth. A node with items is a branch that opens the next level; a node without is a leaf, and only a leaf can be picked. value defaults to label. A disabled row shows but cannot be picked or opened. Property, not attribute. |
placeholder | placeholder | stringstring | undefined | Text shown while nothing is picked. | |
readOnly | readonly | boolean | false | Display only: the trigger keeps its name and its tab stop, opens nothing and clears nothing, and the value still posts. Neither a read-only nor a disabled control holds the form on valueMissing. |
required | required | boolean | false | Marks the control required for form validation. |
showPath | show-path | boolean | false | The trigger shows the branches above the pick as well, Canada / Ontario / Toronto, for a leaf whose label alone is ambiguous. The leaf stays whole and the branches give way first when the box is narrow. |
size | size | "lg" | "md" | "sm" | 'md' | Trigger height: sm 28, md 34 (default), lg 42, from the --control-* tokens. In the quiet variant it is the height of the box that paints on hover and while open; the text keeps the surrounding font. |
tone | tone | "inverse" | undefined | Ink for the quiet variant on the brand chrome. inverse takes the inverse inks, the inverse hover wash and the inverse focus colour, as sui-button's tone of the same name does, so the value reads on a tone="brand" toolbar. Opening the field shows its shell in the card's own inks. The default variant ignores it. | |
value | value | SuiKeynumber | string | undefined | The picked leaf's value, as the leaf gives it (a number stays a number); '' after a clear. The element finds the leaf, and the branches above it, from it. Written as an attribute it is text, and it still finds the leaf whose value reads the same. Mutable; written before suiChange fires. | |
variant | variant | "default" | "quiet" | 'default' | default draws the field shell. quiet draws the picked leaf as text in the surrounding font, with no border or fill at rest, for a record's properties column: the fill comes on hover, the focus ring on focus, and the shell only while the menu is open. The label is not drawn but still names the control, so the host prints the property name. Keyboard, events, validation and the form post are the same in both. Anything else falls back to default. |
Events
| Event | Detail | React | Vue | Description |
|---|---|---|---|---|
suiChange | SuiKeynumber | string | onSuiChange | @sui-change | Fires when a leaf is picked; detail is the new value: the leaf's value as given, or '' after a clear. |
suiClose | SuiCloseReason"cancel" | "close-button" | "escape" | "outside" | "pick" | "scrim" | "trigger" | onSuiClose | @sui-close | Fires before the element closes the menu itself; detail is 'pick' | 'escape' | 'outside' | 'trigger'. Cancelable: preventDefault() keeps it open. Tab reports 'outside', because focus left the control and SuiCloseReason has no member for that. Does not bubble: a preventDefault() in a dialog's own suiClose listener cannot veto this one. |
suiOpen | void | onSuiOpen | @sui-open | Fires after the element opens the menu itself (trigger click, ArrowDown, Enter, Space). Does not bubble, so a dialog around the control never takes it for its own. |
suiOpenPathChange | number[] | onSuiOpenPathChange | @sui-open-path-change | Fires after the element moves openPath itself (hover, click or an arrow key over a branch, the back row, or an open from the trigger); detail is the new path. The prop is written first, so a framework binding stays in step. It reports a cursor move inside the menu, not a destination, which is why it is not suiNavigate. Does not bubble: it reports the panel's state, not the value, like suiOpen and suiClose. |
Methods
| Method | Signature | Description |
|---|---|---|
setFocus() | setFocus() => Promise<void> | Moves keyboard focus to the trigger, as el.focus() does. |
Types
SuiCascadeOption interfaceOne option of the tree. A node with items is a branch and opens the next level; a node without is a leaf, the only kind that can be picked. | |||
|---|---|---|---|
label | string | Text of the row: what a screen reader hears and what the trigger shows once picked. | |
value? | SuiKey | What value holds when this leaf is picked, as given: a number stays a number through value, suiChange and a framework binding, and the form posts its text. Defaults to label. Keep leaf values unique across the tree: the element finds the pick by its value. A branch's value is never posted. | |
disabled? | boolean | Shows the row dimmed: it cannot be picked or opened, the arrow keys step over it and it is announced as disabled. | |
icon? | string | Font Awesome class drawn before the label, in the row and, for the picked leaf, in the trigger. | |
items? | SuiCascadeOption[] | The next level. Present, even empty, the node is a branch; an empty branch opens the empty line. | |
SuiCloseReason typeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own. | |||
|---|---|---|---|
| No members. |
SuiKey typeA record's or an item's key wherever the library identifies one: a row, a tree node, a list item, a selected value. Numbers stay numbers, so a numeric id round-trips through selection, the events and a framework binding without turning into a string. | |||
|---|---|---|---|
| No members. |
Theming
Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.
Color23
Typography1
Spacing and size5
Radius2
Elevation2
Motion3
Every name above is listed with its value and its role in Foundations.