Dropdown holding an expandable tree. Single mode highlights the picked row and closes on pick; multiple draws a checkbox per row and keeps the panel open. value holds node keys and the trigger shows their labels. Built for the chart of accounts, where the user picks a parent account from a hierarchy. Form-associated: posts one entry per key.
The panel is a role="tree" of treeitem rows with aria-level and aria-expanded, and the trigger keeps focus while pointing at the active row through aria-activedescendant. Rows carry aria-selected in single mode and aria-checked when multiple.
A branch on the path to a match opens itself for as long as the query stands. Filtering never writes expanded, so clearing the box restores the expansion the user had.
<div style="max-width:340px;min-height:280px">
<sui-tree-select id="ts-filter" label="Parent account" filter open empty-message="No account matches that name."></sui-tree-select>
</div>
// setup(root): properties and events
setup(root) {
const el = root.querySelector('#ts-filter');
el.options = accountTree;
}
As a property
variant="quiet" draws the picked labels as text in the surrounding font, with no shell at rest: the fill comes on hover, the focus ring on focus and the shell only while the panel is open, which keeps a 200px floor under a value narrower than that. The label is not drawn but still names the control.
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.
<div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;align-items:start">
<sui-tree-select id="ts-help" label="Parent account" help="Sub-accounts inherit the parent detail type."></sui-tree-select>
<sui-tree-select id="ts-req" label="Parent account" required placeholder="Choose an account"></sui-tree-select>
<sui-tree-select id="ts-err" label="Parent account" required error="Parent account is required."></sui-tree-select>
<sui-tree-select id="ts-off" label="Parent account" disabled help="Locked while the period is closed."></sui-tree-select>
</div>
While loading is set a spinner stands in for the chevron and the panel shows the loading row instead of rows. clearable draws an X that clears the pick, or every pick when multiple, and size is the same sm, md, lg vocabulary as the rest of the control family.
Rows are counted after expansion and filtering, so a tree with every branch open windows and the same tree collapsed does not. Rows are one flat list carrying aria-level, aria-posinset and aria-setsize, which is how a window can cut across levels and a screen reader still hears where a row sits.
emptyMessage is the row for a list that has nothing in it and emptyFilterMessage the row for a search that found nothing; they are two states and two sentences, so "Nothing here yet." is never shown over a long list the reader has just narrowed to none.
suiExpandedChange carries every expanded key and suiChange the picked key or keys. suiChange bubbles like any field change; suiExpandedChange reports the panel and does not. suiOpen fires after the element opens itself; the cancelable suiClose fires before it closes, with pick, escape, outside or trigger.
Opens the panel. Once open, moves the active row through the visible rows.
ArrowRight
Expands the active branch, or steps into its first child when it is already open. Under dir="rtl" the pair swaps, so the arrow that points along the reading direction is always the forward one.
ArrowLeft
Collapses the active branch, or climbs to the parent row.
HomeEnd
Moves to the first or last visible row. In the search box the caret keeps them.
EnterSpace
Opens the panel, or picks the active row; with multiple, toggles it. In the search box Space types a space.
Escape
Closes the panel and returns focus to the trigger.
Tab
Closes the panel and moves on.
DeleteBackspace
On the trigger, with clearable set, clears the pick, or every pick when multiple.
API
<sui-tree-select>
Properties
Property
Attribute
Type
Default
Description
clearable
clearable
boolean
false
X control to clear the pick, or every pick when multiple. Delete or Backspace on the trigger does the same.
disabled
disabled
boolean
false
Disables the trigger. A disabled control posts nothing and never holds the form.
emptyFilterMessage
empty-filter-message
stringstring | undefined
Row shown when the filter matched nothing. An empty list and a search that found nothing are different states and one sentence cannot be right about both: "Nothing here yet." is wrong over a list of two hundred the reader has just narrowed to none.
emptyMessage
empty-message
stringstring | undefined
Row shown when the node list is empty.
error
error
stringstring | undefined
Error message; paints the trigger in the danger tone and sets aria-invalid.
expanded
property only
SuiKey[]
[]
Keys of expanded branches. Mutable; updated before suiExpandedChange fires. Property, not attribute.
filter
filter
boolean
false
Search box at the top of the panel. Matching rows keep their ancestors, and a branch on the path to a match opens itself for as long as the query stands. Filtering never writes expanded, so clearing the box restores the expansion the user had.
help
help
stringstring | undefined
Help text under the control. 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 control.
loading
loading
boolean
false
Shows the tree is still on its way: a spinner stands in for the chevron and the panel shows the loading row instead of rows. Set it while fetching, clear it when options land.
loadingMessage
loading-message
stringstring | undefined
Text of the loading row.
multiple
multiple
boolean
false
Checkbox rows and an array value. Every node toggles on its own, branch or leaf, and picking keeps the panel open.
name
name
stringstring | undefined
Form control name. Reflected: the form entry is built from the content attribute.
open
open
boolean
false
Opens the panel. Mutable and the single source of truth: suiOpen announces an open the element performed, and the cancelable suiClose asks before it closes itself on a single-mode pick, outside click, Escape and Tab.
options
property only
SuiTreeSelectNode[]
[]
Node tree (key, label, icon, children). 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 picks still post. Neither a read-only nor a disabled control holds the form on valueMissing.
required
required
boolean
false
A pick is required for the surrounding form to be valid.
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
SuiChoiceValueSuiKey[] | number | string | undefined
Picked node key, or an array of keys when multiple, each as the node gives it (a number stays a number); '' after a single-mode clear. Written as an attribute it is text, and it still picks the node whose key reads the same. Mutable; updated before suiChange fires. The trigger shows the matching labels, joined with commas when multiple.
variant
variant
"default" | "quiet"
'default'
default draws the field shell. quiet draws the picked labels 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 panel 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.
virtualScrollThreshold
virtual-scroll-threshold
number
200
Visible row count at which the panel starts mounting only the rows near the viewport instead of all of them, so a tree showing exactly this many is windowed. Rows are counted after expansion and filtering, so a deep tree with every branch open windows and the same tree collapsed does not. The keyboard, the ids and the counts a screen reader reads still cover every visible row. Set it to 0 to keep every row in the DOM whatever the tree costs, or to 1 to window a short tree under test.
Events
Event
Detail
React
Vue
Description
suiChange
SuiChoiceValueSuiKey[] | number | string
onSuiChange
@sui-change
Fires when the pick changes; detail is the new value: the node's key (single, '' after a clear) or the array of keys (multiple).
Fires before the element closes the panel itself; detail is 'pick' | 'escape' | 'outside' | 'trigger'. Cancelable: preventDefault() keeps it open, so detail === 'pick' is how a consumer keeps a single-mode panel up after a choice. 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.
suiExpandedChange
SuiKey[]
onSuiExpandedChange
@sui-expanded-change
Fires when a branch in the panel opens or closes; detail is the array of expanded keys. Does not bubble: it reports the panel's state, not the value, like suiOpen and suiClose.
suiOpen
void
onSuiOpen
@sui-open
Fires after the element opens the panel itself (trigger click, ArrowDown, Enter, Space). Does not bubble, so a dialog around the control never takes it for its own.
Methods
Method
Signature
Description
setFocus()
setFocus() => Promise<void>
Moves keyboard focus to the trigger, as el.focus() does.
Types
SuiChoiceValuetypeWhat a choice element holds: one option's value, or, where it takes several (multiple), the array of the picked values. A value keeps the type its option gave it, so a numeric id comes back a number through value, the events and a framework binding; only the form posts text.
No members.
SuiCloseReasontypeWhy 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.
SuiKeytypeA 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.
SuiTreeSelectNodeinterfaceTree node: key is the value identity, label is what the row and the trigger show. The key stays as given: a number stays a number through value, suiChange and a framework binding, and the form posts its text.
key
SuiKey
label
string
icon?
string
Font Awesome class drawn before the label (single mode).
children?
SuiTreeSelectNode[]
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.