Skip to content

Tree

A tree is a nested navigation list built on native <details>/<summary>: branches expand and collapse, leaves are real <a href> / <button>. It works with zero JavaScript — toggling, keyboard (Enter/Space on a branch), focus order, and navigation are all native. Put role="list" on every <ul> and wrap the whole thing in <nav aria-label="…">.

It is deliberately not an ARIA role="tree" widget — that pattern requires a full keyboard model (Up/Down between items, Left/Right to collapse/expand, type- ahead) that this component doesn’t ship. So it’s a styled nested-disclosure navigation tree, and every control is independently Tab-focusable. The selected leaf uses aria-current (page for an <a href>, true for a <button>).

Live example
<nav class="re-tree" aria-label="Documentation">
  <ul class="re-tree__list" role="list">
    <li>
      <details class="re-tree__branch">
        <summary class="re-tree__row re-tree__summary">
          <span class="re-tree__label">Getting started</span>
        </summary>
        <ul class="re-tree__list" role="list">
          <li>
            <a class="re-tree__row re-tree__leaf" href="#install">
              <span class="re-tree__label">Install</span>
            </a>
          </li>
          <li>
            <a class="re-tree__row re-tree__leaf" href="#usage">
              <span class="re-tree__label">Usage</span>
            </a>
          </li>
        </ul>
      </details>
    </li>
    <li>
      <details class="re-tree__branch" open>
        <summary class="re-tree__row re-tree__summary">
          <span class="re-tree__label">Components</span>
        </summary>
        <ul class="re-tree__list" role="list">
          <li>
            <details class="re-tree__branch" open>
              <summary class="re-tree__row re-tree__summary">
                <span class="re-tree__label">Forms</span>
              </summary>
              <ul class="re-tree__list" role="list">
                <li>
                  <a class="re-tree__row re-tree__leaf" href="#input">
                    <span class="re-tree__icon" aria-hidden="true">
                      <svg
                        viewBox="0 0 16 16"
                        width="16"
                        height="16"
                        fill="none"
                        stroke="currentColor"
                        stroke-width="1.5"
                      >
                        <rect x="2" y="5" width="12" height="6" rx="1" />
                      </svg>
                    </span>
                    <span class="re-tree__label">Input</span>
                  </a>
                </li>
                <li>
                  <a class="re-tree__row re-tree__leaf" href="#select" aria-current="page">
                    <span class="re-tree__icon" aria-hidden="true">
                      <svg
                        viewBox="0 0 16 16"
                        width="16"
                        height="16"
                        fill="none"
                        stroke="currentColor"
                        stroke-width="1.5"
                      >
                        <path d="M5 6.5 8 9.5 11 6.5" stroke-linecap="round" />
                      </svg>
                    </span>
                    <span class="re-tree__label">Select</span>
                  </a>
                </li>
              </ul>
            </details>
          </li>
          <li>
            <a class="re-tree__row re-tree__leaf" href="#button">
              <span class="re-tree__label">Button</span>
            </a>
          </li>
        </ul>
      </details>
    </li>
    <li>
      <a class="re-tree__row re-tree__leaf" href="#changelog">
        <span class="re-tree__label">Changelog</span>
      </a>
    </li>
  </ul>
</nav>

data-variant="lines" on the .re-tree root adds vertical guide lines.

Live example
<nav class="re-tree" data-variant="lines" aria-label="Files">
  <ul class="re-tree__list" role="list">
    <li>
      <details class="re-tree__branch" open>
        <summary class="re-tree__row re-tree__summary">
          <span class="re-tree__label">src</span>
        </summary>
        <ul class="re-tree__list" role="list">
          <li>
            <details class="re-tree__branch" open>
              <summary class="re-tree__row re-tree__summary">
                <span class="re-tree__label">components</span>
              </summary>
              <ul class="re-tree__list" role="list">
                <li><a class="re-tree__row re-tree__leaf" href="#a">tree.css</a></li>
                <li><a class="re-tree__row re-tree__leaf" href="#b">menu.css</a></li>
              </ul>
            </details>
          </li>
          <li><a class="re-tree__row re-tree__leaf" href="#c">index.css</a></li>
        </ul>
      </details>
    </li>
  </ul>
</nav>

data-density="compact" tightens the rows.

Live example
<nav class="re-tree" data-density="compact" aria-label="Settings">
  <ul class="re-tree__list" role="list">
    <li>
      <details class="re-tree__branch" open>
        <summary class="re-tree__row re-tree__summary">
          <span class="re-tree__label">Account</span>
        </summary>
        <ul class="re-tree__list" role="list">
          <li>
            <button class="re-tree__row re-tree__leaf" type="button" aria-current="true">
              <span class="re-tree__label">Profile</span>
            </button>
          </li>
          <li>
            <button class="re-tree__row re-tree__leaf" type="button">
              <span class="re-tree__label">Security</span>
            </button>
          </li>
        </ul>
      </details>
    </li>
  </ul>
</nav>
  • Expand a branch by default with the native open attribute. Branches open independently (no <details name> — single-open siblings would be an accordion, not a tree).
  • Indentation is structural (each nested <ul> adds one --re-tree-indent step), so it can’t desync from the markup and mirrors correctly in RTL.
  • Tune metrics by overriding --re-tree-indent, --re-tree-row-min, etc. on the root or any subtree.
  • Leaf labels can be a bare text node or a .re-tree__label span (the span adds ellipsis truncation). The optional .re-tree__icon slot adds width before the label, so use icons consistently within a level (all sibling rows or none) to keep labels aligned.
  • A <button> leaf is an in-app action — wire your own click handler; only <a href> leaves navigate on their own.
  • Keyboard — entirely native, so every control is independently Tab-focusable in DOM order. Enter / Space on a branch <summary> toggles it open or closed; Enter follows an <a href> leaf; Enter / Space activates a <button> leaf. There is intentionally no Arrow / Home / End / typeahead roving between rows — see Notes.
  • Focus — a visible :focus-visible ring marks the active row. It’s an inset ring (overriding the default outer one) because a tree is a tight, often scrolled container where an outer ring would clip at the edges.
  • Semantics — branches are native <details>/<summary>, leaves are real <a href> / <button>, and every <ul> carries role="list" so Safari / VoiceOver still announce “list, N items” under list-style: none. Wrap the tree in <nav aria-label="…"> to expose it as a named landmark. The selected leaf uses aria-currentpage for an <a href> document, true for a <button> action — which AT announces as “current”.
  • Notes — this is deliberately a styled nested-disclosure tree, not an ARIA role="tree" widget: that role demands a full roving keyboard model (Up/Down between items, Left/Right to collapse/expand, typeahead) this component doesn’t ship, and claiming the role without the model would mislead AT. The optional .re-tree__icon is decorative (aria-hidden="true"), so it’s skipped in the accessible name. Under forced-colors / Windows High Contrast the selected leaf is repainted with the system Highlight color (plus its medium weight) so “you are here” survives. See the accessibility guide.