The tree Role for Category and File Browsers
Permalink to "The tree Role for Category and File Browsers"Hierarchies are everywhere in data products: folder trees in file managers, category trees in catalogues, account hierarchies in finance, org charts in HR tools, metric trees in analytics. The ARIA tree pattern gives screen reader users the structure — level, position, expanded or collapsed — and keyboard users a compact arrow-key model. Built from nested lists with click handlers, the same hierarchy is a long series of links with no sense of depth and no way to collapse anything without a mouse.
This guide belongs to tree views & hierarchical navigation. For hierarchies with columns of data, use a treegrid instead — see building an accessible treegrid with expandable rows.
Spec reference
Permalink to "Spec reference"role="tree"— a widget presenting a hierarchical list; needs a name (aria-labeloraria-labelledby).aria-multiselectable="true"if several items can be selected.role="treeitem"— an item; parent items havearia-expanded="true|false"; leaf items have noaria-expanded.role="group"— wraps a parent’s children; the browser computes level from nesting.aria-level,aria-setsize,aria-posinset— required when the DOM is flat or only partly rendered.aria-selected/aria-checked— selection (one or many) or check state; do not use both for different meanings without care.- SC 1.3.1, 2.1.1, 4.1.2 (A) — structure, keyboard operation and state must be programmatic.
When to use — and when not to
Permalink to "When to use — and when not to"Use a tree for navigating or choosing within a hierarchy where each item is primarily a label: folders, categories, sections. Users expand and collapse branches and select a node, which usually updates content elsewhere.
Do not use a tree when items have several data columns — that is a treegrid. Do not use a tree for site navigation menus with links; a nested list of links inside nav (with disclosure buttons for sections) is simpler and familiar. The misapplication: wrapping an entire navigation sidebar in role="tree", which forces application-style arrow keys onto users who expected to Tab through links.
Annotated code example
Permalink to "Annotated code example"<ul role="tree" aria-label="Inventory categories"> <!-- named tree (SC 4.1.2) -->
<li role="treeitem" aria-expanded="true" aria-selected="false" tabindex="0">
<span>Electronics</span>
<ul role="group"> <!-- children: level from nesting -->
<li role="treeitem" aria-expanded="false" aria-selected="false" tabindex="-1"><span>Laptops</span>
<ul role="group" hidden>…</ul></li>
<li role="treeitem" aria-expanded="true" aria-selected="true" tabindex="-1"><span>Phones</span>
<ul role="group">
<li role="treeitem" aria-selected="false" tabindex="-1">Android</li> <!-- leaf: no aria-expanded -->
<li role="treeitem" aria-selected="false" tabindex="-1">iOS</li>
</ul></li>
</ul>
</li>
<li role="treeitem" aria-expanded="false" aria-selected="false" tabindex="-1"><span>Furniture</span></li>
</ul>
const visible = () => [...tree.querySelectorAll('[role="treeitem"]')].filter(n => !n.closest('[role="group"][hidden]'));
tree.addEventListener('keydown', (e) => {
const items = visible(); const cur = e.target.closest('[role="treeitem"]'); const i = items.indexOf(cur);
const isParent = cur.hasAttribute('aria-expanded'); const open = cur.getAttribute('aria-expanded') === 'true';
const go = (n) => { if (!n) return; e.preventDefault(); cur.tabIndex = -1; n.tabIndex = 0; n.focus(); };
switch (e.key) {
case 'ArrowDown': go(items[i + 1]); break;
case 'ArrowUp': go(items[i - 1]); break;
case 'Home': go(items[0]); break;
case 'End': go(items[items.length - 1]); break;
case 'ArrowRight': e.preventDefault(); if (isParent && !open) setExpanded(cur, true); else if (open) go(firstChild(cur)); break;
case 'ArrowLeft': e.preventDefault(); if (open) setExpanded(cur, false); else go(parentItem(cur)); break;
case 'Enter': case ' ': e.preventDefault(); select(cur); break; // update content elsewhere
}
});
function setExpanded(n, v) { n.setAttribute('aria-expanded', String(v)); n.querySelector(':scope > [role="group"]').hidden = !v; }
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Key | Action | Expected announcement | AT deviations |
|---|---|---|---|
| Tab into tree | Focus the active node | “Inventory categories, tree view, Electronics, expanded, level 1, 1 of 2” | VoiceOver: “outline” in some versions |
| Down / Up | Next / previous visible node | “Laptops, collapsed, level 2, 1 of 2” | — |
| Right on collapsed parent | Expand | “expanded” | — |
| Right on expanded parent | First child | Child name and level | — |
| Left on expanded parent | Collapse | “collapsed” | — |
| Left on child | Parent | Parent name | — |
| Enter / Space | Select | “selected” | — |
| Type a character | Next node starting with it | Node name | See type-ahead guide |
Integration context
Permalink to "Integration context"Type-ahead and large trees are covered in type-ahead in trees and grids and lazy-loading tree nodes with aria-busy. For trees with checkboxes, see multi-select trees with checkboxes. Selecting a node often updates a detail pane, which follows focus handoff in master-detail layouts.
Gotchas
Permalink to "Gotchas"Interactive content inside treeitems. Buttons or links inside a treeitem break the pattern; screen readers in focus mode cannot reach them reliably. Put actions in a context menu (Shift+F10) or a toolbar.
Icons as names. A folder icon and a chevron must be hidden from assistive technology; the treeitem’s name should be the label only.
Selection versus focus. Moving focus should not select in multi-select trees; in single-select trees, selection may follow focus if selecting is cheap. Decide and keep it consistent.
Framework notes
Permalink to "Framework notes"React Aria Tree and Angular CDK CdkTree implement the pattern; MUI X RichTreeView and SimpleTreeView do too. Check that each exposes level, set size and position when virtualized, and that expand icons are hidden from AT. Vue options vary; verify keyboard behaviour against the table above before adopting.
// React Aria Components
<Tree aria-label="Inventory categories" items={items} selectionMode="single">
{function render(item) { return <TreeItem textValue={item.name}>{item.name}{item.children && <Collection items={item.children}>{render}</Collection>}</TreeItem>; }}
</Tree>
Worked example
Permalink to "Worked example"An e-commerce admin used a nested list of links for 1,200 product categories. Keyboard users Tabbed through every visible category to reach the product list, and screen reader users heard “link, Phones, link, Android…” with no levels. Converting the panel to a tree with a single tab stop, arrow navigation and level announcements reduced the path to the product list to two key presses, and screen reader users could now collapse whole branches with Left.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Provide one Tree component with the keyboard model built in and a strict rule that actions live in a context menu or toolbar, never inside items. Document when to use Tree versus TreeGrid versus nested navigation links.
FAQ
Permalink to "FAQ"Should a tree be a ul or divs?
Either, as long as roles are correct. Nested ul and li elements map naturally to tree and group.
Does selection follow focus in a tree?
It can in single-select trees where selecting is cheap. In multi-select trees, focus and selection must be separate.
How deep can a tree go?
As deep as the data needs, but announce levels; beyond five or six levels users struggle regardless of accessibility, so consider breadcrumbs or search.
Can tree items be links?
Avoid it. If activating a node navigates, handle Enter on the treeitem rather than nesting a link.
What about Shift+F10 on tree items?
Use it to open a context menu for node actions, consistent with grids. See the context menu guidance.
Do trees need a visible focus indicator on the whole row?
Yes. The focused node must be visibly distinct from the selected one; both need 3:1 contrast indicators.
Related
Permalink to "Related"- Type-ahead in trees and grids — jumping by name
- Treegrid with expandable rows — hierarchies with columns
- Opening context menus from the keyboard — actions on nodes