Skip to content

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 / checked changed from comma-separated strings to JSON string arrays. The old form expanded="a,b" / checked="a-1,b" no longer works — migrate to expanded='["a","b"]' / checked='["a-1","b"]'. Why: keys containing commas broke comma-string parsing; JSON arrays unify the shape with oas-tree-select's expanded / 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: true to disable a single node's checkbox (skipped by the cascade); use the component-level disabled attribute 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 matches oas-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 sets expanded to 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

Expand allCollapse allScroll to "Child 58"

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; pass expand: false to 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 dragged

Large 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)

Enable motionMotion off (expand arrow toggles instantly)

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

Toggle block-nodeblock-node on: taller row blocks

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

Disable selectableEnable selectableTry clicking a row

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-request is 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 ​

AttributeDescriptionTypeDefault
accordionAccordion (only one sibling branch open at a time)boolean—
auto-expand-parentAuto-expand parents when a child is checkedboolean—
block-nodeFull-row block selection/hover zone (rows one tier taller)boolean—
can-renameMaster 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-allRenders a select-all row above the tree with a tri-state checkbox (requires checkable)boolean—
check-all-labelSelect-all row label (defaults to the i18n select-all copy)string—
check-strategyCheck export strategy: all (default) / parent / child (with checkable cascading)stringall
check-strictlyDecouple parent/child checkingboolean—
checkableWhether to show checkboxesboolean—
checkedSet 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—
dataNode data [{ key, label, children?, disabled?, isLeaf?, loaded?, renamable? }], JSON stringTreeNode[] | string[]
default-expand-allExpand all nodes by default (applies when the expanded attribute is absent)boolean—
directoryDirectory 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 / collapseboolean—
disabledWhole-tree disabled (select/check/expand/drag all inert, browsing kept)boolean—
draggableNodes can be dragged to reorder / reparent; drop shows insertion line / highlight; release emits oas-node-dropboolean—
emptyEmpty-state text (when no data; no-match uses the locale no-match text)string—
expand-triggerNode expansion trigger: toggle (default, arrow only) / node (clicking the node row)stringtoggle
expandedSet 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-namesField-alias JSON (e.g. {"label":"name","value":"id","children":"subs"}; data is not copy-mapped — node-render receives your original objects)string—
filterIn-tree search filter (keyword input; pair with filter-highlight and el.filterNode)string—
filter-highlightHighlight matched fragments (mark tag)boolean—
heightVirtual scroll viewport height (px); setting it enables virtualized rendering for large datastring—
lazyLazy loading: nodes without children and not marked isLeaf / loaded trigger loading on expandboolean—
motionExpand/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-motionboolean—
multipleClick multi-select (Ctrl/⌘-click; selected set is a JSON array)boolean—
row-heightFixed row height when virtualized (px)string32
selectableWhole-tree selection switch; "false" stops click/keyboard selection while expand/check keep workingstringtrue
selectedKey of the selected nodestring—
sizeFive-tier tree font/padding scale (xs~xl, aliases accepted, invalid falls back to medium)stringmedium
totalFull root-node count in windowed mode; 0 or invalid disables windowed (falls back to normal mode)string—
tree-linesTree indentation guide linesboolean—
window-startAbsolute root index (within the full dataset) of the first root node in the slice (windowed mode)string0
windowedServer-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 unchangedboolean—

Property (JS property; see each row for attribute reflection) ​

PropertyDescriptionTypeDefault
loadLazy-load function (property channel, (node) => Promise<children>)(payload: { key: string }) => void | Promise<unknown>—

Events ​

EventDescription
oas-checkCheck state change, detail: { key, checked }
oas-check-allFired when the select-all row toggles, detail: { checked, values }
oas-expandDispatched on node expand/collapse, detail: { key, expanded, node } (node is a data snapshot)
oas-loadLazy loading triggered, detail: { key }; the host refills children and resets the data attribute
oas-load-errorLazy load failed, detail: { key, error } where error is the error message string (loading clears, clickable to retry)
oas-node-dragendDispatched when a drag ends, detail: { dragKey }
oas-node-dragleaveDispatched when a drag leaves a target, detail: { dragKey, dropKey, position }
oas-node-dragoverDispatched while dragging over a target, detail: { dragKey, dropKey, position } (empty area: dropKey empty, position inner)
oas-node-dragstartDispatched when a drag starts, detail: { dragKey }
oas-node-dropNode dropped, detail: { dragKey, dropKey, position }; position is before / after / inner; an empty-string dropKey means moved to the root
oas-node-renameInline 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-renderDispatched 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-selectNode selected, detail: { key, selected }
oas-window-requestEmitted 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 ​

NameDescription
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: true marks an explicit leaf (no expand arrow under lazy loading); loaded: true marks a node as fully loaded (used with children to avoid triggering loading repeatedly); renamable: false disables renaming for that node (used with can-rename; nodes are renameable by default).

Released under the MIT OR Apache-2.0 License.