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.

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
tree-linesTree indentation guide linesboolean—

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 }

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.