use UI

Tree View

Also called file tree, folder tree, hierarchy, nested list

A hierarchical list whose branches can be expanded and collapsed, navigated as one widget.

Example

  • src
    • components
      • header.tsx
    • utils.ts
  • package.json

What the focused node exposes

role="treeitem" tabindex="0"
aria-expanded="true"
aria-level="1" aria-posinset="1" aria-setsize="2"

The whole tree is a single tab stop: exactly one node carries tabindex="0" and every other carries -1. Inside it, Up and Down walk the visible nodes, Right opens a closed branch and then steps into it, Left closes an open one and then steps out to the parent. That asymmetry is the part most rebuilds skip — and it is the reason to think twice before claiming role="tree".

When to use it

  • The structure is genuine containment — folders holding files, categories holding pages, a DOM holding nodes.
  • Depth varies and users need to open one branch without being shown the rest.
  • People return to the same structure often enough to build a mental map of it.

When not to

  • The nesting is two levels and fixed. That is a list with subheadings, and it needs no arrow-key contract.
  • The order is chronological rather than hierarchical. Steps and events are a timeline; nesting them implies containment that is not there.
  • You cannot implement the full keyboard model. A tree that only responds to clicks should be a disclosure list instead — an honest `<ul>` of collapsibles.

Trade-offs

  • Depth is legible: indentation and expansion state answer “where am I” without a breadcrumb.
  • Only the opened branches cost screen space, so a large structure stays workable.
  • One tab stop for the entire widget, however many nodes it holds.
  • The keyboard contract is long — arrows in four directions, Home, End, expand-all, typeahead — and partially implementing it is worse than not claiming the role.
  • Deep indentation eats horizontal space fast, especially on narrow screens.
  • Collapsed branches hide content from search-on-page, so people miss things that are right there.

Accessibility

What this pattern needs in order to work for everyone.

Keyboard
  • The whole tree is one tab stop. Inside it, roving tabindex moves the focusable node: exactly one node has `tabindex="0"`, every other has `-1`.
  • Down and Up move through visible nodes, skipping anything inside a collapsed branch.
  • Right expands a closed branch, then moves to its first child. Left collapses an open one, then moves to its parent. That asymmetry is the part rebuilds usually miss.
  • Home and End jump to the first and last visible node.
Roles and state
  • `role="tree"` on the container, `role="treeitem"` on each node, and `role="group"` on the list of children — the group is what `aria-expanded` refers to.
  • Branch nodes carry `aria-expanded`. Leaf nodes must not: an expandable-looking leaf is announced as a branch that never opens.
  • When the markup is not a real nested list, supply `aria-level`, `aria-setsize` and `aria-posinset` yourself — “level 3, 2 of 5” is how depth is announced.
  • Mark selection with `aria-selected`, and say up front whether the tree allows one selection or several (`aria-multiselectable`).
Focus
Expanding must not move focus on its own; the user asked to see the children, not to go to them. Collapsing a branch that contains focus moves focus to the branch node, or it is lost.

In the wild

  • VS Code explorer A full implementation of the pattern — arrows, typeahead, expand-all — which is why it feels like a native control.
  • Chrome DevTools elements panel A tree over the DOM, with the same key model and per-node actions on the focused row.