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
- app
- components
- ui
- 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.