Tree Views & Hierarchical Navigation
Permalink to "Tree Views & Hierarchical Navigation"Hierarchical data is everywhere in data products: folders and files, product categories, account charts, organisation structures, permission scopes, metric breakdowns. Tree views present these hierarchies compactly, letting users expand only the branches they need. For sighted mouse users, a tree is just indentation and chevrons. For screen reader users, an accessible tree announces where they are — “Phones, expanded, level 2, 2 of 4” — and for keyboard users it offers fast arrow-key movement with a single tab stop. An inaccessible tree is a long list of unlabelled indentation, or worse, hundreds of tab stops.
The users most affected are screen reader users, who need level, position and expanded state; keyboard users, who need one tab stop, arrow keys and type-ahead; switch users, for whom every extra tab stop costs time; and users with cognitive disabilities, who rely on consistent expand and collapse behaviour. The failure mode this topic prevents is a hierarchy that can be seen but not understood or navigated.
This topic belongs to core ARIA & keyboard navigation for data UIs.
WCAG criteria in scope
Permalink to "WCAG criteria in scope"| Criterion | Level | Relevance to trees |
|---|---|---|
| 1.3.1 Info and Relationships | A | Hierarchy, level and position programmatic |
| 1.4.1 Use of Color | A | Selected, checked and focused states not colour-only |
| 1.4.11 Non-text Contrast | AA | Chevrons, checkboxes and focus indicators at 3:1 |
| 2.1.1 Keyboard | A | Full arrow-key contract |
| 2.4.3 Focus Order | A | Focus stays put during lazy loads |
| 2.4.7 Focus Visible | AA | Focused node visibly distinct from selected |
| 4.1.2 Name, Role, Value | A | Expanded, selected and checked states |
| 4.1.3 Status Messages | AA | Loading, empty and cascade messages |
Prerequisites
Permalink to "Prerequisites"- Composite widget roles & states and roving tabindex.
- Expandable rows & nested data, for when a hierarchy needs columns (treegrid).
- ARIA live regions for dynamic data, for loading and cascade messages.
ARIA & HTML spec reference
Permalink to "ARIA & HTML spec reference"| Attribute / role | Valid values | When to apply | Common misuse |
|---|---|---|---|
role="tree" |
— | Container | Unnamed |
role="treeitem" |
— | Each node | Links or buttons nested inside |
role="group" |
— | Children container | Missing, so levels are wrong |
aria-expanded |
true / false |
Parents only | On leaves |
aria-level |
Integer ≥ 1 | Flat or virtualized trees | Off by one |
aria-setsize / aria-posinset |
Integers | Partially rendered branches | Rendered counts instead of totals |
aria-selected / aria-checked |
true / false / mixed (checked) |
Selection or checkbox trees | Both with conflicting meanings |
aria-multiselectable |
true |
Multi-select trees | Omitted |
aria-busy |
true / false |
Loading branches | Left true |
Step-by-step implementation
Permalink to "Step-by-step implementation"Step 1 — Choose the structure (SC 1.3.1)
Permalink to "Step 1 — Choose the structure (SC 1.3.1)"Tree for labelled hierarchies, treegrid when nodes have columns, nested links for site navigation.
<ul role="tree" aria-label="Inventory categories">…</ul>
Step 2 — Roles and states (SC 4.1.2)
Permalink to "Step 2 — Roles and states (SC 4.1.2)"<li role="treeitem" aria-expanded="false" tabindex="-1">Laptops<ul role="group" hidden>…</ul></li>
See the tree role for category and file browsers.
Step 3 — Keyboard contract and type-ahead (SC 2.1.1)
Permalink to "Step 3 — Keyboard contract and type-ahead (SC 2.1.1)"if (e.key === 'ArrowRight' && !open) setExpanded(cur, true);
See type-ahead in trees and grids.
Step 4 — Checkbox trees (SC 4.1.2, 4.1.3)
Permalink to "Step 4 — Checkbox trees (SC 4.1.2, 4.1.3)"p.setAttribute('aria-checked', allOn ? 'true' : allOff ? 'false' : 'mixed');
See multi-select trees with checkboxes.
Step 5 — Lazy branches (SC 2.4.3, 4.1.3)
Permalink to "Step 5 — Lazy branches (SC 2.4.3, 4.1.3)"group.setAttribute('aria-busy', 'true'); group.innerHTML = '<li role="treeitem" aria-disabled="true">Loading…</li>';
See lazy-loading tree nodes with aria-busy.
Step 6 — Library configuration
Permalink to "Step 6 — Library configuration"<Tree aria-label="Inventory categories" selectionMode="single">…</Tree>
See tree views in React Aria and Angular CDK.
Keyboard interaction contract
Permalink to "Keyboard interaction contract"| Key | Action | Expected AT announcement | Failure indicator |
|---|---|---|---|
| Tab | Into / out of tree | Tree name, focused item | Tab moves item to item |
| Down / Up | Next / previous visible item | Name, state, level, position | No level or position |
| Right | Expand, or move to first child | “expanded” | Nothing happens |
| Left | Collapse, or move to parent | “collapsed” or parent name | Focus lost |
| Home / End | First / last visible item | Item name | — |
| Characters | Type-ahead | Matched item | Reader command instead |
| Enter | Activate | Result of the action | — |
| Space | Select or toggle checked | “selected” / “checked” | Arrow keys also toggle |
Screen reader compatibility matrix
Permalink to "Screen reader compatibility matrix"| AT + browser | Level | Position | Mixed state | Notes |
|---|---|---|---|---|
| NVDA + Firefox | Yes | Yes | “partially checked” | Reliable |
| NVDA + Chrome | Yes | Yes | “partially checked” | — |
| JAWS + Chrome | Yes | Yes | “partially checked” | Tree view mode automatic |
| VoiceOver + Safari | On level change | Yes | “mixed” | Calls trees “outline” in some versions |
| TalkBack + Chrome | Sometimes | Sometimes | Recent versions | Test swipe and hardware keyboard |
| Narrator + Edge | Yes | Yes | Yes | — |
Edge cases & failure modes
Permalink to "Edge cases & failure modes"1. Buttons inside tree items
Permalink to "1. Buttons inside tree items"Diagnosis: rename and delete buttons in each node. Fix: a context menu on Shift+F10 or a toolbar.
2. Wrong levels in flat trees
Permalink to "2. Wrong levels in flat trees"Diagnosis: a virtualized flat list of nodes without aria-level. Fix: set level, set size and position explicitly.
3. Expanded but empty
Permalink to "3. Expanded but empty"Diagnosis: lazy branch expands with nothing under it. Fix: loading item, then children or “Empty folder”.
4. Arrow keys toggling selection
Permalink to "4. Arrow keys toggling selection"Diagnosis: multi-select tree where moving checks items. Fix: Space only.
5. Indentation as the only cue
Permalink to "5. Indentation as the only cue"Diagnosis: levels shown visually, not exposed. Fix: nesting with groups or aria-level.
The tree role for category and file browsers
Permalink to "The tree role for category and file browsers"Named tree, treeitems in groups, expanded state on parents, levels and positions, the arrow-key contract and no interactive content inside items. See the tree role for category and file browsers.
<li role="treeitem" aria-expanded="true">Phones<ul role="group">…</ul></li>
Type-ahead in trees and grids
Permalink to "Type-ahead in trees and grids"Buffered characters with a 500 ms timeout, wrap-around matching on names, focus only, disabled with modifiers and in edit mode. See type-ahead in trees and grids.
const match = ordered.find(n => name(n).toLowerCase().startsWith(buffer));
Multi-select trees with checkboxes
Permalink to "Multi-select trees with checkboxes"aria-checked with mixed parents, Space to toggle, cascades that recompute ancestors and announce counts. See multi-select trees with checkboxes.
status.textContent = `Checked 12 reports. 14 selected in total.`;
Lazy-loading tree nodes with aria-busy
Permalink to "Lazy-loading tree nodes with aria-busy"Truthful expanded state, loading placeholders, focus that stays on the parent, empty and error items, full set sizes. See lazy-loading tree nodes with aria-busy.
status.textContent = `${label(item)}: ${total} items`;
Tree views in React Aria and Angular CDK
Permalink to "Tree views in React Aria and Angular CDK"Libraries supply structure and keys; you supply names, text values and announcements. See tree views in React Aria and Angular CDK.
<cdk-tree [dataSource]="categories" aria-label="Inventory categories">…</cdk-tree>
Cross-cutting concerns
Permalink to "Cross-cutting concerns"Search and filter. Trees deeper than three or four levels need a search field. Filtering a tree should keep ancestors of matches visible, expand them, and announce “12 matches in 4 branches”.
Drag and drop. Moving nodes by dragging needs a non-drag alternative — “Move to…” with a destination picker — as in moving rows between groups and lists.
Persisted expansion. Users who expand a deep branch expect it to stay expanded when they return. Store expanded keys per user.
Breadcrumbs. When a tree drives a content area, a breadcrumb of the selected node’s path helps everyone, and gives screen reader users the path without re-reading the tree.
Worked example: a permissions editor
Permalink to "Worked example: a permissions editor"An admin console’s role editor showed 240 permissions as nested lists of native checkboxes grouped by module, with no keyboard collapse and 240 tab stops. A screen reader user configuring a role took over half an hour and could not tell which modules were partly granted. The rebuild used a checkbox tree: one tab stop, Right and Left to expand and collapse modules, Space to toggle, mixed state on modules with partial grants, and cascade announcements (“Checked 18 permissions under Invoices. 52 selected in total.”). A search field filtered to matching permissions with their modules expanded. The same task took under ten minutes.
Who is affected, and how
Permalink to "Who is affected, and how"| User group | Typical failure | Fix |
|---|---|---|
| Screen reader users | No level, position or state | Proper roles and attributes |
| Keyboard users | One tab stop per node | Roving focus; arrows; type-ahead |
| Switch users | Long scanning sequences | One tab stop; search |
| Low-vision users | Focus and selection indistinguishable | Distinct indicators at 3:1 |
| Users with cognitive disabilities | Silent cascades and loads | Announced counts and states |
Common mistakes in existing trees
Permalink to "Common mistakes in existing trees"Audits of tree views in data products find the same problems again and again:
- Every node is a link or button in the tab order. Hundreds of tab stops; no arrow keys. The fix is roving focus with a single tab stop.
- Indentation without structure. Nested
divs with padding and nogrouporaria-level, so every node is announced as level 1. - Chevrons as the only expand control. A separate focusable chevron button per node doubles tab stops and splits the node’s name from its state. Expand with Right and Left on the node itself.
- Selection and focus merged in multi-select trees. Arrowing changes selections the user did not intend.
- Silent lazy loading and cascades. Users cannot tell whether a branch is loading, empty or failed, or how many items a parent checkbox just changed.
- Interactive content inside items. Rename, delete and “more” buttons inside each node, unreachable in focus mode.
Each fix is local, and the first three can usually be resolved by switching to a library tree configured as described in this topic.
Trees inside larger layouts
Permalink to "Trees inside larger layouts"Trees rarely stand alone. In a file manager, a folder tree drives a file list; in an analytics tool, a metric tree drives a chart. The patterns from master-detail & split views apply: selecting a node updates content elsewhere without moving focus, Enter (or an explicit command) moves focus to the content, F6 cycles between the tree pane and the content pane, and a resizable splitter between them follows the window splitter pattern. Name the tree pane and the content pane as regions so landmark navigation works, and keep the selected node visible when focus is in the content so users can see — and hear, via a breadcrumb — where they are.
Measuring tree quality
Permalink to "Measuring tree quality"Two quick numbers summarise a tree’s keyboard accessibility: the number of Tab presses needed to cross it (target: one) and the number of key presses to reach a known deep item with type-ahead or search (target: under ten). For screen reader quality, sample five nodes and check that each announcement includes name, state, level and position. Re-run these checks after library upgrades.
Design system integration
Permalink to "Design system integration"Provide Tree, CheckboxTree and TreeGrid as separate components with a shared collection model, built-in type-ahead, lazy-loading contracts, and announcer integration. Forbid interactive children inside tree items in the component API; offer a context menu slot instead. Tokens for focus and selection indicators must be distinct and verified at 3:1 in light, dark and forced-colours modes.
Testing checklist
Permalink to "Testing checklist"Automated
Permalink to "Automated"Keyboard
Permalink to "Keyboard"Screen reader
Permalink to "Screen reader"FAQ
Permalink to "FAQ"When should I use a tree instead of nested lists?
When users interact with the hierarchy as a widget — expanding, selecting, acting on nodes — inside an application. For site navigation, nested lists of links are better.
Is a tree the same as a treegrid?
No. A treegrid has rows and columns; each row can have several cells. Use it when hierarchy items carry data.
Can trees be virtualized?
Yes, using a flat rendering with aria-level, aria-setsize and aria-posinset on every rendered item.
Should trees support Shift+F10 context menus?
Yes, for node actions. It keeps actions out of items and matches desktop conventions.
How do I show the selected node's location elsewhere?
A breadcrumb or a heading in the content area naming the node and its path.
Do trees need search?
Large or deep ones do. Type-ahead only finds visible nodes.
How do I test a tree quickly?
Tab in, press Down and Right a few times, and listen for level and position. Then press a letter and check type-ahead. Then Tab out — it should take one press.
Should expanding a node announce how many children it has?
For lazy-loaded nodes, yes (“Contracts: 14 items”), because the user is waiting for something. For already-loaded nodes, “expanded” is enough; the first child announces “1 of 14” when the user moves down.
How should "expand all" work?
Offer it as a command for small trees, announce a summary (“Expanded 12 folders”), and avoid it for very large trees, where it creates thousands of nodes. The asterisk key expands all siblings in the ARIA pattern, which is a safer, local version.
Related
Permalink to "Related"- Expandable rows & nested data — treegrids
- Composite widget roles & states — roving focus
- Row actions & context menus — node actions
- Accessible filtering & faceted search — category trees as filters
- Accessible virtualized list patterns — virtualized trees