Skip to content

HyTree ​

Usage examples on this page are written for Lit / plain HTML (<hy-tree>). The same component ships as HyTree in @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-onSelectable parentNon-selectable parentLeaf
'chevron'row → select; chevronrow → expandrow → select
'row'row → select + expandrow → expandrow → select
'double-click'dblclick → expand; clickrow → expandrow → 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-sidebar with hy-sidebar-group sections.
  • A DTCG design-token document: hy-dtcg-tree.

Accessibility: your part ​

  • Give the tree an aria-label that says what the hierarchy holds.
  • Say an item's state in its label. An icon in the leading slot is not announced.

Examples ​

Plain tree ​

html
<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 ​

html
<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 ​

html
<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 ​

PropertyAttributeTypeDefaultDescription
valuevaluestring''Currently selected item's value. Reflected.
multiSelectmulti-selectbooleanfalseMulti-select mode. v1 throws if set to true.
sizesize'small' | 'medium' | 'large''medium'Size variant — forwarded to every hy-tree-item.
expandOnexpand-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 ​

EventDetailDescription
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 ​

SlotDescription
defaulthy-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.

Built with Lit. Documented with VitePress.