HyTree
Usage examples on this page are written for Lit / plain HTML (
<hy-tree>). The same component ships asHyTreein@whitespaceux/harmony-react(native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.
Child components: <hy-tree-item> — used only inside this one.
A role="tree" container implementing the WAI-ARIA Tree View pattern.
hy-tree is the policy-free primitive: it walks its slotted hy-tree-item descendants, wires roving tabindex over visible items (collapsed subtrees skipped), handles the full keyboard matrix (Arrow keys, Home/End, *, first-letter typeahead), promotes child clicks to select / change events, and stamps spatial ARIA attributes (aria-level, aria-posinset, aria-setsize) onto every item during its DOM walk.
Activation (expand-on)
expand-on | Selectable parent | Non-selectable parent | Leaf |
|---|---|---|---|
'chevron' | row → select; chevron | row → expand | row → select |
'row' | row → select + expand | row → expand | row → select |
'double-click' | dblclick → expand; click | row → expand | row → select |
v1 is single-select only; setting multiSelect throws. Drag-to-reorder, async children, and virtualization are out of scope.
Expansion state lives on each hy-tree-item — the tree does not own an expansion map. Imperative methods (expandAll, collapseAll, expandTo) write expanded on items directly and do not fire expand / collapse events; only user-driven toggles fire those. The * key is one: it fires one expand per sibling it opens (none for a sibling already open).
The expand-on policy controls how a row click activates expansion; 'chevron' (the default) preserves Finder-style separation, 'row' makes the whole row toggle expansion (alongside selection), and 'double-click' mimics JetBrains/Finder list-view. Non-selectable parent rows always expand on row click regardless of the policy — their row has no other meaning, so chevron-only would leave it dead.
When to use
- Nested items people browse and open: folders, categories, an outline.
- Picking one or several items in a hierarchy, with
multi-select.
Do
- Open the branches down to the current item.
When not to use
- Items that do not nest:
hy-list. - A few collapsible sections of content:
hy-accordion. - The main navigation of an app:
hy-sidebarwithhy-sidebar-groupsections. - A DTCG design-token document:
hy-dtcg-tree.
Accessibility: your part
- Give the tree an
aria-labelthat says what the hierarchy holds. - Say an item's state in its label. An icon in the
leadingslot is not announced.
Examples
Plain tree
<hy-tree id="nav" aria-label="File system">
<hy-tree-item value="docs" label="Docs">
<hy-tree-item
slot="children"
value="docs/getting-started"
label="Getting started"
></hy-tree-item>
<hy-tree-item slot="children" value="docs/api" label="API reference"></hy-tree-item>
</hy-tree-item>
<hy-tree-item value="src" label="Source">
<hy-tree-item slot="children" value="src/index.ts" label="index.ts"></hy-tree-item>
</hy-tree-item>
</hy-tree>
<script>
const tree = document.getElementById('nav');
tree.addEventListener('select', (event) => {
console.log('selected:', event.detail.value);
});
</script>Per-row overflow menu — the trailing slot is a plain projection point; hy-menu-button stops propagation so chevron / row clicks don't misfire
<hy-tree>
<hy-tree-item value="colors.primary.500" label="500">
<span slot="trailing">
<hy-menu-button>
<hy-icon slot="trigger" name="more-vertical"></hy-icon>
<hy-menu-item label="Rename" value="rename"></hy-menu-item>
<hy-menu-item label="Duplicate" value="duplicate"></hy-menu-item>
<hy-divider></hy-divider>
<hy-menu-item label="Delete" value="delete" variant="danger"></hy-menu-item>
</hy-menu-button>
</span>
</hy-tree-item>
</hy-tree>Shared context menu — one menu bound to every row; the handler reads which item was right-clicked from event.detail.target
<hy-tree id="files">
<hy-tree-item value="readme.md" label="README.md"></hy-tree-item>
<hy-tree-item value="package.json" label="package.json"></hy-tree-item>
</hy-tree>
<hy-context-menu target="#files hy-tree-item">
<hy-menu-item label="Rename" value="rename"></hy-menu-item>
<hy-menu-item label="Copy path" value="copy-path"></hy-menu-item>
<hy-menu-item label="Delete" value="delete" variant="danger"></hy-menu-item>
</hy-context-menu>
<script>
const menu = document.querySelector('hy-context-menu');
menu.addEventListener('select', (event) => {
const row = event.detail.target; // the hy-tree-item right-clicked
console.log(`${event.detail.value} on ${row?.value}`);
});
</script>API
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
value | value | string | '' | Currently selected item's value. Reflected. |
multiSelect | multi-select | boolean | false | Multi-select mode. v1 throws if set to true. |
size | size | 'small' | 'medium' | 'large' | 'medium' | Size variant — forwarded to every hy-tree-item. |
expandOn | expand-on | 'chevron' | 'row' | 'double-click' | 'chevron' | Activation policy for the row's primary click. Forwarded to every hy-tree-item; the behavior matrix is in the component description ("Activation"). In every mode the chevron toggles expansion (and stops propagation) and the keyboard contract is unchanged (Right/Left expand/collapse, Enter/Space activate). Default 'chevron' preserves Finder semantics. |
Events
| Event | Detail | Description |
|---|---|---|
select | — | { value, item, originalEvent } — fires on click / Enter / Space on a selectable item. |
change | — | { value } — fires on every selection change (including programmatic). |
expand | — | { value, item } — bubbles from an item when the user toggles it open; * fires one per sibling it opens. |
collapse | — | { value, item } — bubbles from an item when the user toggles it closed. |
Slots
| Slot | Description |
|---|---|
default | hy-tree-item children (recursive via each item's children slot). |
Methods
expandAll()
Expand every item that has children. Does not fire expand.
collapseAll()
Collapse every expanded item. Does not fire collapse.
expandTo()
Expand every ancestor of the item with value. No-op if the value doesn't match any item.
focusItem()
Move keyboard focus (and the tab stop) to the item with value. No-op if no item matches.