Skip to content

Tree

hc-tree upgrades a semantic nested list into a WAI-ARIA tree: file browsers, category hierarchies, admin navigation. Without JavaScript the markup is a plain readable list and every link keeps working; installTree() adds the roles, a roving tabindex, and the full APG keyboard model. Server-pushed subtrees are the lazy-tree recipe’s job.

  • src
    • app.js
  • README.md

Branch-ness is declared by aria-expanded — present means the item can expand (the server owns the initial value; the behavior toggles it). Levels derive from nesting; there is no aria-level bookkeeping.

Included in the auto-init ./behaviors bundle. Applies role="tree" / treeitem / group and a roving tabindex (one tab stop per tree); re-applies both when subtrees are swapped in by htmx. Expanding an item dispatches a bubbling hc:treeexpand (detail: { item }) — the hook the lazy-tree recipe builds on. It never touches the network. Like every behavior it is idempotent and returns an uninstaller; trees added by a later htmx swap are picked up automatically (MutationObserver), and a per-tree observer re-applies the roles whenever a subtree is swapped in.

KeyAction
↓ / ↑Next / previous visible item (collapsed subtrees are skipped).
Open a closed branch; on an open branch, move to its first child.
Close an open branch; otherwise move to the parent.
Home / EndFirst / last visible item.
Enter / SpaceActivate: follow the label’s link when there is one, otherwise toggle the branch.
a–zType-ahead — jump to the next visible item starting with the character.

← and → mirror in RTL.

StateSelectorEffect
Expanded[aria-expanded="true"]Group visible; chevron rotated.
Collapsed[aria-expanded="false"]Group hidden.
Current[aria-current]Accent row (follows data-color) — a server state, the “you are here” marker.
Loading.hc-tree__group[aria-busy="true"]Spinner in the group while a lazy subtree fetches (behavior-set).

Selection models (aria-selected, multi-select, checkboxes) are deliberate non-goals for now — activation follows real links, and the current node is server-rendered state.

data-size="sm" tightens the indent and font for dense panels.

  • src
    • app.js
  • README.md
  • Nested <ul>/<li> + role="tree" + aria-expanded + roving tabindex is the APG pattern; levels are announced from the structure.
  • The chevron toggles are aria-hidden pointer affordances — the keyboard model duplicates everything they do.
  • Give the tree an aria-label (or aria-labelledby).
  • Focus is drawn on the row (:focus-visible); reduced motion disables the chevron transition and the busy spinner.

Component tokens (in component.tokens.json) — semantic references throughout, so themes, accents and dark mode apply automatically:

Token pathPurpose
tree.indentIndent per nesting level.
tree.row-padding-y / row-padding-xRow padding.
tree.row-radiusRow corner radius.
tree.fgTree text color.
tree.hover-bgHovered-row background.
tree.current-bg / current-fgThe aria-current row accent (follows data-color).
tree.toggle-sizeChevron toggle box.
tree.guideNesting guide line (also the busy-spinner track).
Show the generated CSS variables
--hc-tree-indent
--hc-tree-row-padding-y | -row-padding-x | -row-radius
--hc-tree-fg | -hover-bg
--hc-tree-current-bg | -current-fg
--hc-tree-toggle-size | -guide