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"
Tree versus treegrid versus nested navigation Comparison of the three structures for hierarchies: a tree for labelled items, a treegrid for items with data columns, and nested navigation links for site menus. Tree versus treegrid versus nested navigation✗ Tree (role="tree")Items are labels: folders, categoriesOne tab stop; arrows navigateExpand with Right, collapse with LeftSelection updates content elsewhereIn-app hierarchies✓ Treegrid or nested navigationTreegrid: items have columns of dataTreegrid: cell navigation within rowsTreegrid: expand on the first cellNested nav: links with disclosure buttonsNested nav: site menus, Tab between links

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.

Choosing the right hierarchy widget Flow: do nodes carry several data values? Treegrid. Is this site navigation of links? Nested lists of links with disclosures. Otherwise a tree; add checkboxes for multi-select and lazy loading for large branches. Choosing the right hierarchy widgetSeveral data values per node?size, owner, modifiedyes: treegridSite navigation of links?sections and pagesyes: nested links in navOtherwiselabelled hierarchytreeMulti-select?permissions, categoriesaria-checked with mixedLarge branches?thousands of childrenlazy load with setsize

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.

Finding an item in a deep tree Chain: type in the tree's search field; matching branches expand; the status reports matches; Down moves into the filtered tree; Enter opens the item; the breadcrumb shows its path. Finding an item in a deep treeSearch field"travel"Filtered treeancestors of matchesexpandedStatus"6 matches in 3branches"TreeDown to "Travelexpenses"filterannouncenavigateBreadcrumbExpenses › Operating› Travel expenses
Search complements type-ahead for items inside collapsed branches.

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 no group or aria-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.

Permalink to "Related"

← Back to Core ARIA & Keyboard Navigation for Data UIs