TreeSelect
Tree-structure selection supporting parent-child cascaded multiple selection, search filtering, lazy loading and tag-based display.
Single Select
Single select
Click a node to select it and close the dropdown; children can be expanded/collapsed. Keyboard: Tab to focus, Enter/Space/↑/↓ to open, ↑/↓ to move the highlight, Enter/Space to select, →/← to expand/collapse, Esc to close.
Multiple Select (parent-child linkage + tags)
Multiple (multiple)
In multiple mode, selecting a parent cascades the selection to all children; clicking again cancels. Parent nodes show an "all / half" selected state. Selected values are displayed as tags (chips) inside the trigger: click the × on a tag to remove it; press Backspace while the trigger is focused to remove the last one.
Visual change note: since v2.4 the multiple display switched from joined text ("a、b and N more") to tags (the industry-standard form with per-tag removal) — a breaking visual change; use
max-tag-countfor collapsed display.
Search Filter (filterable)
Search filter (filterable)
With filterable, a standalone search box appears at the top of the panel: matched nodes show together with their ancestors (path context) and all descendants (lenient match); an empty state shows when nothing matches. Inside the search box: ↑/↓ move the highlight (it lands on the first match), Enter selects, Esc closes the panel and returns focus to the trigger. The keyword is cleared after selecting by default; reserve-keyword keeps it. Typing emits oas-search (detail.value is the keyword — pair with loading for remote search). Custom matching: el.filter = (label, option) => boolean.
Controlled Search Input (input-value)
input-value (controlled search text)
input-value makes the search text controllable: when the attribute is present it is the source of truth for the search box (presets/external updates sync into the box and re-filter); user typing writes the attribute back and dispatches oas-input-value-change (detail: { value }) — the host can listen or simply read the attribute.
Parent-Child Decoupling (check-strictly)
check-strictly
Cascaded: value = —
Decoupled: value = —
multiple cascades parent-child by default (checking a parent checks all children). With check-strictly, checks no longer cascade, there is no half state, and value is exactly the checked set (check-strategy has no effect then) — covering the "independent multi-select at any level" scenario.
Check Strategy (check-strategy)
check-strategy controls which nodes enter value in multiple mode: all (default) keeps both parents and children; parent keeps only parent nodes (a parent represents its fully-checked children); child keeps only leaf nodes. All three strategies share the same cascaded checking logic — only the emitted value differs:
Check strategy (all / parent / child)
all: value = —
parent: value = —
child: value = —
After checking "Frontend", the three strategies emit: all → ["fe","framework","vue","react","css"], parent → ["fe"], child → ["vue","react","css"] (order follows the demo data's declaration order of "Framework" children). Under parent, checking leaves one by one converges the value to the parent once all its children are checked.
Lazy Loading (lazy / load)
Lazy loading (lazy + load)
With lazy, nodes without children and not marked isLeaf / loaded are treated as "not loaded": expanding shows a spinner, emits oas-load (detail.value) and calls el.load({ value }); after the host merges children into the data and re-sets options, the spinner disappears. Explicit leaves are marked with isLeaf: true. Right example: a not-yet-loaded value js is preset; cache-data (JSON: [{ value, label }]) makes the display show its label instead of the raw value.
Path Display (show-path)
Path display (show-path + separator)
show-path displays the selected value as its full "root → self" path (e.g. Frontend / Framework / Vue), useful to disambiguate same-named nodes across subtrees; separator customizes the separator (default " / ").
Tag Management (max-tag-count / max)
Tag collapsing and selection limit
value =
value = —
max-tag-count limits the tags shown in the trigger; the rest collapse into +N (hover the title to list hidden items). max limits how many nodes can be selected (counted by checked nodes); once reached, unchecked nodes appear disabled while checked ones can still be unchecked.
Clearable & Affixes (clearable / prefix / suffix)
clearable + prefix / suffix
With clearable, a clear button shows when there is a value (clicking clears and emits oas-clear). prefix-text / suffix-text render affix texts inside the trigger; template[slot="prefix"] / [slot="suffix"] provide custom content. prefix / suffix are compatibility aliases (canonical: prefix-text / suffix-text), usable through both the attribute and property channels (React / Vue bindings included).
Custom Suffix Icon (suffix-icon slot)
suffix-icon (replaces the default arrow)
template[slot="suffix-icon"] replaces the default dropdown arrow on the trigger (rotating with the container when expanded).
Value Carries Label (label-in-value)
label-in-value (value carries { value, label } objects)
With label-in-value, the value attribute and the oas-change detail.value carry { value, label } objects (an object in single mode, an object array in multiple mode) — no more host-side reverse lookup when a form submit needs "value + label"; out-of-tree preset values can echo their label directly from the object. FormData submission stays plain strings.
Size & Status (size / status)
size tiers + status
size: small / medium (default) / large — height and font follow the global size tokens. status: error / warning / success validation borders (the host aria-invalid attribute remains supported, equivalent to error).
Field Names (field-names)
field-names
When backend field names differ, declare the mapping of label / value / children / disabled to raw fields via field-names (JSON) — no need to transform the whole tree on the host side.
Controlled Open (open)
Controlled open (open + oas-open-change)
When the open attribute is present the open state is controlled (a value of false means controlled-closed): internal clicks no longer change the state directly; they only emit oas-open-change (detail.open), and the host decides by writing the open attribute. Without open the component is uncontrolled (the event is still emitted, e.g. to preload data on first open).
Expand Behavior (default-expand-all / tree-lines / expand-trigger)
Expand all by default / tree lines / node click
default-expand-all expands all nodes on first open (an explicit expanded attribute takes precedence); tree-lines shows indent guide lines; with expand-trigger="node", clicking a parent label expands it in addition to selecting (default toggle expands only via the arrow).
Custom Node Rendering
oas-node-render event
template[slot=node]
Two channels: listen to oas-node-render (detail: { node, element, level, expanded, selected, checked }) and mutate element in the handler; or provide a static skeleton via template[slot="node"] with [data-node-label] auto-bound to the node label (same channel as oas-tree). Tree ARIA (treeitem / aria-level) is unaffected by custom rendering.
Panel Chrome (header / footer / loading / empty)
Panel header/footer slots + loading mask
template[slot="header"] / [slot="footer"] insert arbitrary content at the top / bottom of the panel; loading shows a loading mask (pair with remote search and lazy loading); the empty state text can be customized via the empty attribute or the template[slot="empty"] slot (built-in localized text by default).
Flat Data (host utility)
Backend flat {id, pId} → tree
The component intentionally has no flat-data attribute — converting a backend flat list on the host side is a one-line reduce (zero friction); the conversion function used by this page is shown in the script below.
Preset Value
Preset value
Controlled Expand (expanded)
expanded is a JSON array declaring the set of node values to expand (a controlled channel): externally changing the property immediately reflects in the dropdown tree. Below, "Frontend → Framework" is pre-expanded, and buttons drive it externally:
Controlled expand (expanded)
Presetting expanded='["fe","framework"]' expands "Frontend" and "Framework" the first time the dropdown opens; clicking "Expand all" externally writes expanded='["fe","framework","be"]', and "Collapse all" writes '[]', re-rendering immediately when the dropdown opens.
Large Data (Virtual Scroll)
Ten-thousand-node virtual scroll
Setting virtual enables windowed rendering (with height for the viewport and item-height for the row height): the dropdown reuses oas-virtual-list to render only the visible window, so 10,100 nodes (100 departments × 100 members) scroll smoothly; ↑/↓ move the highlight, Enter toggles a check, →/← expand/collapse, and aria-activedescendant stays valid across window re-renders. filterable composes with virtual scroll (filtered results feed the window). Data is injected via the options property channel, the expanded set is controlled by the expanded attribute, and "Expand all" writes the full node set externally.
Popup Positioning (scroll container)
Inside a scroll container
Tree select inside an overflow container:
↓ Scroll down: the panel is fixed-positioned to the trigger and not clipped by the container; it flips up automatically when the viewport lacks space.
The dropdown uses fixed positioning + viewport computation anchored below the trigger (width aligned with the trigger, auto flip when space is insufficient), so it is never clipped by overflow containers such as modals and scroll areas. Fine-tune via ::part(dropdown).
Disabled
Disabled
Empty State
No data
Custom empty text (empty) + keep keyword (reserve-keyword)
Events
Change events
Listen to oas-change: detail.value is a string for single select and an array for multiple; detail.labels carries the display labels in value order (paths when show-path is on) — hosts needing "value + label" take them from here instead of object-value binding. Other events: oas-clear (cleared, detail.value is the previous value), oas-search (search input), oas-load (lazy load), oas-open-change (open state), oas-node-render (node rendering).
Mobile form (bottom sheet)
On touch screens (coarse pointer) or narrow viewports (<768px), the panel is automatically hosted by an oas-bottom-sheet bottom sheet: rises from the viewport bottom + backdrop + drag handle swipe-down to close (backdrop click / Esc also close), with bottom safe-area inset; the search box and tree scroll inside the sheet; the desktop form keeps the original floating dropdown.
required
required (native validation chain)
required drives the native validation chain (form-associated): when unfilled, checkValidity() returns false (valueMissing) and native form submission is blocked; it recovers to :valid once filled.
Programmatic read/write of the current value goes through the public value property: el.value reads the selected value string in single mode (empty string when nothing is selected) or an array of selected values in multiple mode (label-in-value objects are lifted to plain values, matching the FormData submission semantics); el.value = x writes the controlled value attribute and re-renders the label/chips (arrays are JSON-serialized automatically, label-in-value keeps the object contract shape, empty values remove the attribute). No events are dispatched.
API
oas-tree-select
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
cache-data | Fallback cache (JSON) for echoing values with no matching option label | string | [] |
check-strategy | Multiple-select check strategy: all (default) keeps both parents and children in the value; parent keeps only parent nodes (a parent represents its fully-checked children); child keeps only leaf nodes | string | all |
check-strictly | Decouple parent/child checking in multi-select | boolean | — |
clearable | Clearable (fires oas-clear) | boolean | — |
default-expand-all | Expand all nodes by default (applies when the expanded attribute is absent; frozen after the first internal toggle) | boolean | — |
disabled | Disabled | boolean | — |
empty | Custom empty-state text/content | string | — |
expand-trigger | Node expansion trigger: toggle (default, arrow only) / node (clicking the node row expands) | string | toggle |
expanded | Set of expanded node values (JSON array, controlled) | string | — |
field-names | Field-alias JSON (e.g. {"label":"name","value":"id","children":"subs"}; options are not copy-mapped — node-render receives your original objects) | string | — |
filterable | Searchable (dedicated search box at the top of the panel) | boolean | — |
height | Virtual-scroll viewport height (px); works with virtual | string | 288 |
input-value | Controlled search text (typing writes back and emits oas-input-value-change) | string | — |
item-height | Fixed row height for virtual scroll (px) | string | 36 |
label-in-value | value and oas-change detail carry { value, label } objects (FormData still submits plain values) | boolean | — |
lazy | Lazy loading (pair with the el.load function property; nodes carry isLeaf) | boolean | — |
loading | Panel loading state (during lazy load / remote search) | boolean | — |
max | Multi-select limit (by checked-set size) | string | — |
max-tag-count | Collapse multi-select tags beyond the count into +N (title lists hidden items) | number | — |
multiple | Multiple select + parent-child cascade | boolean | — |
name | Form field name (key for native FormData submission; also the key of same-name multiple entries in multiple mode; the browser skips submission when name is absent) | string | — |
open | Controlled open (presence = controlled; "false" = controlled-closed); flips fire oas-open-change; in the mobile form (touch / viewport <768px) the open panel is hosted by an oas-bottom-sheet bottom sheet (swipe-down/backdrop/Esc to close); desktop keeps the floating dropdown | string | — |
options | Tree options, JSON array, supports children / disabled | TreeOption[] | string | [] |
placeholder | Placeholder text | — | — |
prefix-text | Trigger prefix content (slot="prefix" accepts any content) | string | — |
required | Required marker (drives the native valueMissing validation chain; not passed through to the inner control) | boolean | — |
reserve-keyword | Keep the search keyword after selecting a node (cleared by default) | boolean | — |
separator | Path separator (default /, pair with show-path) | string | / |
show-path | Echo the full path (initial echo and multi-select labels use paths) | boolean | — |
size | Size preset small / medium (default) / large | string | medium |
status | Validation status: error / warning / success | string | — |
suffix-text | Trigger suffix content (slot="suffix" likewise) | string | — |
tree-lines | Tree indentation guide lines | boolean | — |
value | Selected value (JSON array in multiple mode) | string | string[] | null | — |
virtual | Enable virtual scroll: the dropdown renders only the visible window for large data (reuses oas-virtual-list), keeping keyboard/ARIA intact | boolean | — |
Property (JS property; see each row for attribute reflection)
| Property | Description | Type | Default |
|---|---|---|---|
value | Current value: a value for single; an array of values for multiple | string | string[] | null | — |
Events
| Event | Description |
|---|---|
oas-change | Selection change, detail: { value } |
oas-clear | Fires on clear; detail is the pre-clear value |
oas-input-value-change | Dispatched on search-text change, detail: { value } |
oas-load | Fires when a lazy node expands to load, detail: { value } |
oas-node-render | Fires on node render (custom node rendering channel), detail: { node, element, level, expanded, selected, checked } |
oas-open-change | Open state flips, detail: { open } |
oas-search | Fires on search input, detail: { value } |
Slots
| Name | Description |
|---|---|
template[slot="empty"] | Custom empty-state content |
template[slot="node"] | Custom node template ([data-node-label] binds the label) |
template[slot="suffix-icon"] | Custom trailing trigger icon (replaces the default arrow) |
CSS Variables
| CSS Variable | Default |
|---|---|
--oas-glass-blur | none |
--oas-glass-ring | transparent |