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-label or aria-labelledby). aria-multiselectable="true" if several items can be selected.
  • role="treeitem" — an item; parent items have aria-expanded="true|false"; leaf items have no aria-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.
A small category tree as screen readers expose it Hierarchy of an inventory category tree: Electronics expanded with Laptops and Phones, Phones expanded with Android and iOS, and Furniture collapsed; each node shows its level and position. A small category tree as screen readers expose itElectronicslevel 1, 1 of 2, expandedLaptopslevel 2, 1 of 2, collapsedPhoneslevel 2, 2 of 2, expandedAndroidlevel 3, 1 of 2iOSlevel 3, 2 of 2Furniturelevel 1, 2 of 2, collapsed
Level, position and expanded state come from nesting or explicit attributes.

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.

Nested links versus a tree widget Comparison: nested links in a nav suit site navigation with Tab movement; a tree widget suits in-app hierarchies with arrow keys, levels and expand state. Nested links versus a tree widget✗ Nested links in a navTab moves link to linkDisclosure buttons for sectionsLevel implied by indentationFamiliar for site navigationActivation navigates✓ role="tree"Arrows move node to node; one tab stopRight and Left expand and collapse"level 2, 3 of 5" announcedExpected for file and category browsersSelection updates content in place

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
Tree announcements across readers Matrix of NVDA, JAWS, VoiceOver and TalkBack against announcing level, position in set and expanded state for tree items. Tree announcements across readersLevelPositionExpanded stateNVDA + ChromeYesYesYesJAWS + ChromeYesYesYesVoiceOver + SafariOn changeYesYesTalkBack + ChromeSometimesSometimesYes
Level and position are the parts most often missing in custom trees.

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.

Permalink to "Related"

← Back to Tree Views & Hierarchical Navigation