English
Tree
Displays hierarchical data with support for expand / collapse, selection, cascading checks, multi-select by click, search filtering, lazy loading, keyboard operation, and node drag-and-drop.
⚠️ Breaking change (v2.5):
expanded/checkedchanged from comma-separated strings to JSON string arrays. The old formexpanded="a,b"/checked="a-1,b"no longer works — migrate toexpanded='["a","b"]'/checked='["a-1","b"]'. Why: keys containing commas broke comma-string parsing; JSON arrays unify the shape withoas-tree-select'sexpanded/value. If your keys may contain commas, rebuild the attribute using the array form when upgrading.
Basic Usage
Hierarchical tree
Click the expand arrow to show / hide child nodes; click a node label to select it. The tree is fully keyboard operable (↑/↓ move focus, → expand, ← collapse, Home/End, Space toggles a checkbox, Enter activates).
Touch adaptation: the expand arrow stays visually 20px, while its hit area grows to the 44px touch target under coarse pointers (via an invisible ::before overlay), keeping both tap accuracy and visual density.
Controlled Expansion and Selection
Initial expansion and selection
expanded is a JSON string array (e.g. '["a","b"]'), selected is the selected node key. It composes with default-expand-all (expand everything on first render; an explicit expanded wins), auto-expand-parent (automatically fill in ancestors when a deep key is injected externally), and expand-trigger="node" (clicking a node also expands / collapses it).
Checkboxes (Cascading)
Cascading checks (parent selects all, half state shown)
Checked set (checked attribute, JSON array): —
With checkable the model is cascading (same semantics as oas-tree-select): checking a parent checks all its checkable children; when every child of a parent becomes checked the parent converges to checked; partial children show the parent checkbox in half state (indeterminate).
check-strategy:all(default, parents + children all in the value) /parent(collapse fully-checked children into their parent) /child(leaves only);check-strictly: decouple parent/child — no cascading, no half state;- Data can mark
disableCheckbox: trueto disable a single node's checkbox (skipped by the cascade); use the component-leveldisabledattribute to disable all interaction.
Non-cascading checks (check-strictly) & default expand
check-strictly disables cascading; default-expand-all expands everything initially; auto-expand-parent auto-fills ancestor expansion when the host writes the expanded attribute (checking / clicking does not trigger it).
Empty state (empty text & empty slot)
The empty attribute customizes the empty text; rich content can go in template[slot="empty"].
Multi-Select by Click
Multi-select (multiple, click again to deselect)
With multiple, selected becomes a JSON string array and clicking a label appends / removes its key (click again to deselect). Checkboxes (checkable) and click-selection (multiple / selected) are two independent models and can be combined.
Expansion Behaviour
Accordion + tree lines + expand on node click
accordion: expanding a node collapses its siblings at the same level (mutually exclusive);tree-lines: renders ancestor indent guides (attribute name matchesoas-tree-select);expand-trigger="node": clicking the node text also expands / collapses (default"toggle"only reacts to the arrow);auto-expand-parent: when the host setsexpandedto a deep key (setAttribute('expanded', '["deep-key"]')), the component automatically fills in the ancestors.
Search Filtering
In-tree filtering (keeps matched branches; optional highlight)
filter is the search term (attribute-driven; the host places the search box itself, here wired to an oas-input). Matches keep all their ancestors (path context) and the whole matched subtree, independent of the expanded state; no match shows the empty state. filter-highlight wraps matched slices of the default label text in <mark>. Use the filterNode(label, node) property for custom matching (default: case-insensitive label containment).
Field Mapping
Field aliases (field-names)
Use field-names (a JSON object mapping the keys key/label/children/disabled/isLeaf/loaded to your data field names) when your data shape differs — normalization happens once at the data entry and shares the same field machinery as oas-tree-select.
Disabled States
Node-level disabled / selectable / disableCheckbox
- Node
disabled: true: the whole row is inert (no click / expand / check); - Node
selectable: false: excluded from click and keyboard selection, while checkboxes and expansion still work; - Node
disableCheckbox: true: the checkbox is disabled and skipped by the cascade; - Component-level
disabled: disables all interaction on the whole tree (read-only tree).
Imperative Methods and Scrolling
Expand all / collapse all / locate a node
Imperative methods (data stays host-controlled; the methods only recompute the expansion set / scroll position):
expandAll()/collapseAll(): expand / collapse every node;expand(keys: string[])/collapse(keys: string[]): expand / collapse the given keys;scrollTo(key, { expand?: boolean }): scroll the node into view (expands the ancestor chain by default; passexpand: falseto leave the expanded state untouched).
Lazy Loading
Lazy loading (async child fill-in on expand)
Click the expand arrow before "Folder A" to trigger loading
Lazy load failure retry (oas-load-error)
Click "Fails first"'s expand arrow to see the error event and retry
With lazy, nodes without children and not marked isLeaf / loaded are considered "not loaded": clicking the expand arrow emits oas-load (detail: { key }) and calls the load callback; the host refills the child nodes and resets the data attribute. During loading, a loading placeholder is shown in place of the expand arrow (replaceable via template[slot="toggle-loading"]); after the fill-in completes, the node auto-expands and the placeholder is removed. When the load callback returns a Promise that rejects, the component clears the loading state, rolls the expansion back, and emits oas-load-error (detail: { key, error }) — the node is clickable again to retry. Nodes marked isLeaf: true do not show an expand arrow.
Node Drag and Drop
Draggable (reorder / reparent)
Drag a node to the upper / lower / middle of a target row to insert before / after / inside it
With draggable, nodes can be dragged to reorder or reparent: dropping on the top 25% of a target row inserts before it (before), the bottom 25% inserts after it (after), and the middle (when the target is expandable) moves it inside (inner; when the target is not expandable the middle falls back to before / after by the upper / lower half). An insertion line and highlight feedback are shown during the drag; on release, oas-node-drop is emitted (detail: { dragKey, dropKey, position }) and the host updates the data and resets the data attribute. Dropping on empty tree space moves the node to the root (dropKey empty string, position: 'inner').
The full drag lifecycle is also observable: oas-node-dragstart (detail: { dragKey }), oas-node-dragover / oas-node-dragleave (detail: { dragKey, dropKey, position }; dropKey is empty and position is 'inner' over blank space), and oas-node-dragend (detail: { dragKey }) — useful for custom drag hints or cross-tree interactions.
Guard the drop targets with the property callbacks (rejected during the drag — no insertion line feedback):
js
tree.allowDrop = ({ dragKey, dropKey, position }) => dropKey !== 'grp-1' // disallow dropping into this node
tree.allowDrag = (node) => node.key !== 'item-1' // this node cannot be draggedLarge Data Sets (Virtual Scroll)
Virtual scroll with 10k nodes
Setting height enables virtualization (with a fixed row-height): the tree reuses oas-virtual-list to render only the nodes in the visible window. The expanded state is kept in the expanded attribute and survives scrolling and re-renders; keyboard navigation and scrollTo() keep the window in sync.
Custom Node Rendering
Custom nodes (icon + rich text)
Node content can be provided as a static skeleton via template[slot="node"] (the [data-node-label] node is bound to the node label automatically), and template[slot="toggle"] replaces the default expand arrow. After each node row renders, oas-node-render is emitted (detail: { node, element, level, expanded, selected, checked }); the host can listen and rewrite element into any icon / rich text. In this example the task-count badge is written by that event. Keyboard access and ARIA (treeitem / aria-expanded / aria-level) stay intact under custom rendering.
Directory Mode (File Browser)
Directory mode (file browser style)
With directory, the tree renders in a file-browser style: nodes with children (or not yet loaded under lazy) show a folder icon, nodes with isLeaf / no children show a file icon; the folder icon switches between collapsed / expanded, and the depth-based indent and hover row highlight follow the row styles. Combine with tree-lines to show ancestor guide lines.
Node Renaming
Inline renaming (double-click or F2, Enter commits / Esc cancels)
Double-click "R&D team" or one of its children, or press F2 after selecting a node
With can-rename, node renaming is enabled: double-click a node label (or press F2 with the node focused) enters the inline editing state; Enter / blur commits, Esc cancels (restores the old value without emitting). While editing, arrow keys stay inside the input so tree roving keyboard navigation is unaffected; mark a node renamable: false in the data to disable renaming for that node individually.
Data stays host-controlled: on commit the component only emits oas-node-rename (detail: { key, label, oldLabel }) and never mutates the data model — the host listens, writes the new label back into the data, and resets the data attribute; only then does the row show the new name. If the host does nothing, the old value is kept (no desync from one-sided component mutation).
Expand / Collapse Transition
Expand/collapse motion (toggle to compare)
With motion, expand/collapse animates with a height transition (off by default): expanding grows the newly added child rows smoothly from zero height to their natural height; collapsing first shrinks the child rows away before removing them. Duration and easing follow the --oas-transition-* tokens. In virtual-scroll mode (a height is set) the expand entrance degrades to a fade and collapse switches instantly — virtual rows use a fixed height where height animation would cause jank and misalignment. The animation respects prefers-reduced-motion (auto-disabled when the user asks for reduced motion).
Expand / Collapse Event
oas-expand (node expand / collapse)
Last action: Click an expand arrow
Every expand/collapse interaction (toggle arrow, expand-trigger="node", keyboard →/←) emits oas-expand with detail: { key, expanded, node } — node is a snapshot of the data node, so hosts can drive other views from it.
Events
Selection and check events
Selected: — · Checked: —
Five Size Tiers (size)
size scales the whole tree's font size and row padding across five tiers: xs / small / medium / large / xl (the sm / md / lg aliases are accepted as equivalents; invalid values fall back to medium). medium is the default tier (identical to not setting the attribute).
Size tiers compared
Full-row block selection area (block-node)
block-node renders rows as taller block-level hit areas: the selection/hover band already covers the whole row, and enabling this adds one tier of row height — useful for dense pointing (touch screens, frequent selection). Off by default (compact rows).
block-node toggle
Whole-tree selection switch (selectable)
selectable toggles row selection for the whole tree (default on, current behavior); setting "false" stops click/keyboard selection and no oas-select is dispatched — expanding and checking are unaffected. The node-level selectable: false data field still excludes individual nodes.
selectable switch
Select all (check-all)
check-all (effective together with checkable) renders a select-all row above the tree: checking it selects every checkable node, a partial selection shows an indeterminate checkbox, and clicking again clears the selection. disabled / disableCheckbox nodes are excluded. The default label is "Select all"; override it with check-all-label. Each toggle emits oas-check-all (detail: { checked, values }).
check-all select all
Toggle the "Select all" row or check items one by one to see the tri-state; click again to clear.
Server-side root window (windowed)
Very large trees (hundreds of thousands of roots): the host supplies only the root-node slice near the visible window and the component holds no full root set. The expected root window is derived from the flattened visible rows (8 roots of margin on both sides); when not covered,
oas-window-requestis emitted for the host to refill, and rows render by absolute root index.
Attributes: windowed + total (full root count) + window-start (absolute root index of the slice's first root); data carries the root slice only (children travel with their root; lazy loading unchanged). Event oas-window-request: detail: { start, end, reason } (half-open, absolute root indices; reason = init/scroll/resize/data), the same expected window is not emitted twice.
ARIA in windowed mode: root nodes report aria-setsize = total and aria-posinset = window-start + rootIndex + 1 (absolute); children report their loaded sibling count. Selection state is mirrored to aria-selected (counts follow the visible set when filtering).
Boundaries of local capabilities in windowed mode: local filter and top-level check-all are unavailable (skipped with a devWarn — filter/select on the full dataset on the host side, then refill); keyboard navigation at the slice start/end emits a window request; row drag cannot cross unloaded roots. Refill atomically as "data first, then window-start" (the component anchors scrolling during refill, but a host that replaces the slice wholesale on every request may ping-pong across the margins; widening the window is recommended).
Refill with a widened window rather than an exact replacement (otherwise the start edge may converge toward 0 step by step with the margin).
For the full host recipe (segment fetch, abort in-flight, merge by id, atomic refill) and a runnable example, see Server-side windowed data.
API
oas-tree
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
accordion | Accordion (only one sibling branch open at a time) | boolean | — |
auto-expand-parent | Auto-expand parents when a child is checked | boolean | — |
block-node | Full-row block selection/hover zone (rows one tier taller) | boolean | — |
can-rename | Master switch for node renaming: when on, double-click a node label or press F2 to enter inline editing; Enter commits / Esc cancels / blur commits; committing emits oas-node-rename (mark a node renamable: false in the data to exclude it individually) | boolean | — |
check-all | Renders a select-all row above the tree with a tri-state checkbox (requires checkable) | boolean | — |
check-all-label | Select-all row label (defaults to the i18n select-all copy) | string | — |
check-strategy | Check export strategy: all (default) / parent / child (with checkable cascading) | string | all |
check-strictly | Decouple parent/child checking | boolean | — |
checkable | Whether to show checkboxes | boolean | — |
checked | Set of checked node keys (JSON string array, e.g. '["a"]'; the pre-v2.5 comma string is deprecated and falls back to an empty set with a warning) | string | — |
data | Node data [{ key, label, children?, disabled?, isLeaf?, loaded?, renamable? }], JSON string | TreeNode[] | string | [] |
default-expand-all | Expand all nodes by default (applies when the expanded attribute is absent) | boolean | — |
directory | Directory mode: nodes with children (or not yet loaded under lazy) show a folder icon, nodes with isLeaf / no children show a file icon; the folder icon switches on expand / collapse | boolean | — |
disabled | Whole-tree disabled (select/check/expand/drag all inert, browsing kept) | boolean | — |
draggable | Nodes can be dragged to reorder / reparent; drop shows insertion line / highlight; release emits oas-node-drop | boolean | — |
empty | Empty-state text (when no data; no-match uses the locale no-match text) | string | — |
expand-trigger | Node expansion trigger: toggle (default, arrow only) / node (clicking the node row) | string | toggle |
expanded | Set of expanded node keys (JSON string array, e.g. '["a"]'; the pre-v2.5 comma string is deprecated and falls back to an empty set with a warning) | string | — |
field-names | Field-alias JSON (e.g. {"label":"name","value":"id","children":"subs"}; data is not copy-mapped — node-render receives your original objects) | string | — |
filter | In-tree search filter (keyword input; pair with filter-highlight and el.filterNode) | string | — |
filter-highlight | Highlight matched fragments (mark tag) | boolean | — |
height | Virtual scroll viewport height (px); setting it enables virtualized rendering for large data | string | — |
lazy | Lazy loading: nodes without children and not marked isLeaf / loaded trigger loading on expand | boolean | — |
motion | Expand/collapse height transition animation (off by default): non-virtual mode transitions row-container max-height on expand/collapse, virtual-scroll mode degrades to a fade-in on expand with instant collapse; duration/easing follow --oas-transition-*, disabled under prefers-reduced-motion | boolean | — |
multiple | Click multi-select (Ctrl/⌘-click; selected set is a JSON array) | boolean | — |
row-height | Fixed row height when virtualized (px) | string | 32 |
selectable | Whole-tree selection switch; "false" stops click/keyboard selection while expand/check keep working | string | true |
selected | Key of the selected node | string | — |
size | Five-tier tree font/padding scale (xs~xl, aliases accepted, invalid falls back to medium) | string | medium |
total | Full root-node count in windowed mode; 0 or invalid disables windowed (falls back to normal mode) | string | — |
tree-lines | Tree indentation guide lines | boolean | — |
window-start | Absolute root index (within the full dataset) of the first root node in the slice (windowed mode) | string | 0 |
windowed | Server-side root-node window mode (boolean, off by default): with total / window-start, data carries only the root-node slice (children travel with their root; lazy loading unchanged); when the expected root window is not covered, oas-window-request is emitted for the host to refill. Non-windowed behavior unchanged | boolean | — |
Property (JS property; see each row for attribute reflection)
| Property | Description | Type | Default |
|---|---|---|---|
load | Lazy-load function (property channel, (node) => Promise<children>) | (payload: { key: string }) => void | Promise<unknown> | — |
Events
| Event | Description |
|---|---|
oas-check | Check state change, detail: { key, checked } |
oas-check-all | Fired when the select-all row toggles, detail: { checked, values } |
oas-expand | Dispatched on node expand/collapse, detail: { key, expanded, node } (node is a data snapshot) |
oas-load | Lazy loading triggered, detail: { key }; the host refills children and resets the data attribute |
oas-load-error | Lazy load failed, detail: { key, error } where error is the error message string (loading clears, clickable to retry) |
oas-node-dragend | Dispatched when a drag ends, detail: { dragKey } |
oas-node-dragleave | Dispatched when a drag leaves a target, detail: { dragKey, dropKey, position } |
oas-node-dragover | Dispatched while dragging over a target, detail: { dragKey, dropKey, position } (empty area: dropKey empty, position inner) |
oas-node-dragstart | Dispatched when a drag starts, detail: { dragKey } |
oas-node-drop | Node dropped, detail: { dragKey, dropKey, position }; position is before / after / inner; an empty-string dropKey means moved to the root |
oas-node-rename | Inline rename committed (Enter / blur), detail: { key, label, oldLabel }; data stays host-controlled — the component does not mutate the model, so the new label only shows once the host updates data (keeps the old value otherwise; Esc cancel does not emit) |
oas-node-render | Dispatched for each rendered node row, detail: { node, element, level, expanded, selected, checked } (element is the node label container; the host can rewrite it into icon / rich text) |
oas-select | Node selected, detail: { key, selected } |
oas-window-request | Emitted in windowed mode when the expected root window is not covered by the loaded range; detail: { start, end, reason } (half-open, absolute root indices; reason is init / scroll / resize / data). The same expected window is not emitted twice |
Slots
| Name | Description |
|---|---|
template[slot="empty"] | Custom empty-state content |
template[slot="node"] | Static row template cloned into each node label container; the [data-node-label] node is bound to the node label automatically |
template[slot="toggle"] | Static expand button template cloned into each expandable node's toggle button (replaces the default › icon) |
template[slot="toggle-loading"] | Lazy-expanding indicator (replaces the spinner) |
Node field notes:
isLeaf: truemarks an explicit leaf (no expand arrow under lazy loading);loaded: truemarks a node as fully loaded (used withchildrento avoid triggering loading repeatedly);renamable: falsedisables renaming for that node (used withcan-rename; nodes are renameable by default).