Tree

Hierarchical list with expand / collapse and single-select. Markup is a plain nested <ul> of <li data-lb-node> elements; the component wraps each node in a focusable row with a chevron, an optional icon, and the label. Annotated in place: role="tree" / role="treeitem" with aria-expanded and aria-level; selection is class-only (.lb-tree__row--selected). The tab stop and focus live on the .lb-tree__row inside each <li>, so the focused element itself carries none of that state.

a11y (built in): the nested <ul> is annotated in place — role="tree" on the root, role="treeitem" on every node, role="group" on each child list, an aria-level per depth, and aria-expanded on every parent, kept in sync by clicks, keys and expandAll() / collapseAll(). A roving tab stop means one Tab enters the tree and one leaves it: ↑↓ walk the visible rows only, → expands or steps to the first child, ← collapses or steps to the parent, Home/End jump to first/last visible, Enter selects, Space toggles. Chevron and node icon are aria-hidden decoration, and the focused row takes the system focus ring through :focus-visible.

Usage: data-lb-tree auto-inits on DOMContentLoaded and rebuilds the rows in place — you author only the nested <ul> / <li data-lb-node> skeleton, with data-lb-expanded for initially-open parents and data-lb-value when the selection key should outlive a label rename (label text is the default). Both events bubble from the root: lb-tree-select with detail.value / detail.label on commit, and lb-tree-expand with detail.value / detail.expanded on every toggle, including those triggered by the API. That API hangs off el._lbTree — expandAll(), collapseAll(), getSelected(), setSelected(value); Tree is not exported on the LB namespace, so the data attribute is the only init path — call LB.init() after injecting a tree at runtime.

Keyboard: Tab focuses the tree, then ↑/↓ to move, ← to collapse or jump to parent, → to expand or jump to first child, Enter to select, Space to toggle expand, Home/End for first/last visible.


Default — file explorer

Per-node data-lb-icon renders a glyph next to the label. data-lb-expanded opens a node initially. The Tree handles arbitrary nesting depth — this demo goes six levels deep so you can see the indentation scales without breaking.

  • Documents
    • Resume.pdf
    • Photos
      • vacation.jpg
      • family.jpg
      • portrait.png
    • Notes.md
  • Projects
    • letbe-ds
      • src
        • components
          • Button
            • Button.tsx
            • Button.test.tsx
            • index.ts
          • Card
            • Card.tsx
            • index.ts
        • hooks
          • useTheme.ts
          • useMedia.ts
      • README.md
      • package.json
    • website
      • index.html
      • styles.css
  • Music
    • favourites.mp3

Plain text — no icons

Skip data-lb-icon for a minimal tree of category names. The chevron alone carries the parent affordance.

  • Engineering
    • Frontend
      • Web
      • Mobile
    • Backend
    • Platform
  • Design
    • Brand
    • Product
  • Operations

Custom values — data-lb-value

Each row's selection identity defaults to its label text. Set data-lb-value for a stable key (e.g. an id or path) that won't change if the label is renamed.

Selected: —
  • Books
    • Fiction
    • Non-fiction
  • Music
    • Classical
    • Jazz

Expand / collapse all + selection API

Three methods on el._lbTree: expandAll(), collapseAll(), and setSelected(value) for restoring saved state.

  • Documents
    • Resume.pdf
    • Photos
      • vacation.jpg
  • Music
    • song.mp3