HomeJournalThis post

Accessible Tree View React Tutorial

A state-first React tree view with stable IDs, roving focus, complete keyboard behavior, semantic roles, dynamic loading, filtering, and reducer tests.

JP
JP Casabianca
UI/UX designer and full-stack engineer · Bogotá

An accessible tree view React component is a focus-management system before it is a recursive list. If identity, visible order, expansion, and selection are entangled, the keyboard will eventually land on a hidden or different node.

This tutorial defines a stable state model, complete arrow-key behavior, semantic DOM binding, dynamic-content policy, and reducer tests. It also names when a simpler disclosure list is the more accessible choice.

Define the accessible tree view React state model

An accessible tree view React component represents a hierarchy with one keyboard focus target, optional selection, and expandable branches. Keep focused ID, selected IDs, expanded IDs, disabled state, and the underlying node graph distinct. Conflating focus with selection makes arrow navigation mutate the user's choice; conflating expansion with data loading makes a failed request collapse a branch without explanation.

Derive the visible linear order from the graph and expanded set, then move focus through that order. The worked reducer fixture exposes parent relationships and proves Down moves from the root to its first child when expanded. When the branch is closed, hidden children disappear from navigation and focus remains on a visible item rather than an orphaned DOM node.

The reducer’s source of truth is a graph plus sets of stable identifiers; visible order is derived for each action. I test insertion before the focused row, removal of the focused row, collapse of an ancestor, and reparenting during refresh, with an explicit fallback for each case so a rerender never chooses a new focus target by incidental array position.

Accessible tree view React focus mapOne roving focus target moves through visible tree items while selection and expansion remain independent states. ▾ ProjectOverview▾ Sourceindex.tstabindex=0
  • Declared input
  • Inspectable transformation
  • Measured output
Figure 1: Focus belongs to one visible ID; expansion changes order without changing selection implicitly.

Use semantic roles in accessible tree view React

The container has role=tree; each interactive node has role=treeitem; nested sets use role=group. Provide aria-expanded only for nodes that can expand, aria-selected only when selection exists, and an accessible name from visible text. DOM nesting can communicate level, set size, and position, or explicit aria-level, aria-setsize, and aria-posinset can do so when rendering is flattened.

ARIA treeview roles are a promise of behavior, not a styling shortcut. If the interaction is ordinary disclosure navigation, a list of buttons and links may be simpler. Choose a tree only when users need hierarchical arrow navigation and the product can implement the complete pattern.

The semantic contract comes directly from the APG tree-view pattern and the WAI-ARIA specification. A component story displays the computed role, level, position, size, expansion, and selection beside every node, making a disagreement between flattened DOM structure and announced hierarchy visible during review.

Stabilize accessible tree view React focus

Use one tabindex=0 tree item and tabindex=-1 for the rest, updating the pair whenever focus moves. This roving tabindex lets Tab enter the widget once and leave once, while arrows operate inside it. Store focus by node ID rather than array index so insertions and sorting do not jump to another item.

When the focused node is removed or hidden by collapse, move to the nearest visible ancestor or next documented fallback before the DOM update completes. Do not reset to the first node after every render. The focus map should show keyboard focus, selection, hover, and expansion independently, because each state needs a visible style and a separate test.

Roving focus is exercised from Tab entry through several arrow moves and back to Tab exit, with an assertion that exactly one visible item is tabbable after every transition. The test then deletes that item before the browser focus effect runs, proving the fallback is resolved from state and not by querying a stale DOM index.

Runnable artifact: The reducer fixture derives visible order from expansion and moves focus by stable node ID.

Save this proof as tree-focus-reducer.test.mjs and run node tree-focus-reducer.test.mjs. Expected final line: PASS: tree focus stable.

import assert from "node:assert/strict";
const nodes=[{id:"a",parent:null},{id:"b",parent:"a"},{id:"c",parent:"a"}];
const visible=(open)=>nodes.filter(n=>!n.parent||open.has(n.parent)).map(n=>n.id);
const move=(id,key,open)=>{const ids=visible(open),i=ids.indexOf(id);return key==="Down"?ids[Math.min(i+1,ids.length-1)]:key==="Up"?ids[Math.max(i-1,0)]:id};
assert.equal(move("a","Down",new Set(["a"])),"b");assert.equal(move("a","Down",new Set()),"a");
console.log("PASS: tree focus stable");

Complete accessible tree view React keyboard behavior

Down and Up move to the next and previous visible items. Right expands a closed branch or moves to its first child; Left collapses an open branch or moves to its parent. Home and End reach the first and last visible items.

Enter or Space performs the declared activation or selection action, and printable characters may support typeahead with a bounded buffer. Tree keyboard navigation should respect text direction for horizontal meaning only where the pattern defines it; vertical order remains stable. Prevent default scrolling only for keys the tree actually handles. Test the keyboard matrix from leaf, open branch, closed branch, first item, last item, disabled item, and dynamically loaded branch, not merely from the root.

The keyboard matrix is generated from node kind and boundary: leaf, closed branch, open branch, first visible item, last visible item, disabled item, and loading branch. Expected outcomes cite the APG keyboard guidance, while optional typeahead and product-specific activation remain separately labeled choices.

Follow accessible tree view React primary guidance

The WAI-ARIA Authoring Practices Tree View pattern documents expected roles and keyboard interaction. The WAI-ARIA 1.2 Recommendation defines the states and properties, while WCAG 2.2 supplies requirements such as focus visibility and target usability. Use assistive-technology testing to supplement, not replace, that normative reading.

A screen reader announcing level, expanded state, and name is useful evidence, but it cannot reveal every sighted-keyboard focus defect. Conversely, a unit-tested reducer cannot prove DOM roles, live loading announcements, contrast, zoom behavior, or whether the visible label matches the accessible name.

Normative reading, reducer tests, and assistive-technology trials answer different questions. The specification establishes states, the pure tests establish navigation transitions, and browser sessions establish focus placement and announcements; the release packet keeps all three so one passing layer cannot be used to waive a failure in another.

KeyClosed branchOpen branchLeafBoundary
RightExpandFirst childNo moveNo scroll
LeftParentCollapseParentNo scroll
DownNext visibleNext visibleNext visibleStay last
HomeFirstFirstFirstFirst
Figure 2: The keyboard contract is stated by starting state and visible result.

Handle dynamic accessible tree view React content

Loading children should preserve focus on the parent, expose a busy state, and announce success or failure without inserting a surprise focus stop. Filtering needs a product decision: show ancestors of matches, flatten results into a list, or preserve the tree with hidden branches. Never leave aria-setsize claiming nodes that cannot be reached.

Virtualization is especially risky because assistive technology may need positional context beyond the mounted window. Continue fundamentals through browser focus, naming via accessible names and descriptions, composite focus patterns in an accessible combobox, and lifecycle recovery in an accessible drawer. Those patterns share focus discipline without pretending the widgets are interchangeable.

Dynamic loading keeps focus on the parent and exposes busy and error states without inventing child rows. Filtering is demonstrated in two modes—a hierarchy-preserving view with ancestors and a flat search-results list—so the product selects one truthful interaction instead of retaining tree roles after hierarchy has effectively disappeared.

Test accessible tree view React as a state machine

Unit-test the pure reducer or navigation functions for every key and boundary, then render interaction tests that assert focus, attributes, visible order, and activation. Add relation tests: inserting a sibling before the focused node keeps the same focused ID; collapsing another branch does not move focus; renaming a node updates typeahead but not identity; and filtering either relocates focus according to policy or leaves it unchanged when visible. Run keyboard-only and screen-reader passes in supported browser combinations, plus zoom and high-contrast checks.

The reducer artifact intentionally remains framework-neutral JavaScript so its logic can run without a DOM. The article's React boundary begins where state is bound to semantic elements and effects restore focus.

Rendered tests assert the active element, visible IDs, relevant attributes, and selection after each key, then repeat after data mutation. A manual pass at zoom and in forced colors follows the applicable WCAG 2.2 requirements, with screenshots that distinguish focus, selection, hover, and expansion without relying on color alone.

  1. 1Derive

    Flatten only nodes visible through expanded ancestors.

  2. 2Reduce

    Apply one key to stable focused and expanded IDs.

  3. 3Render

    Bind roles, states, labels, groups, and one focus target.

  4. 4Verify

    Assert DOM focus, announcement, and visible state together.

Figure 3: Pure navigation state binds to semantic DOM and returns focus after every dynamic change.

Release accessible tree view React with interaction evidence

The receipt includes use-case justification, node schema, stable-ID source, visible-order algorithm, focus fallback, selection model, expansion and loading model, keyboard matrix, typeahead policy, roles and properties, labels, error announcements, focus styles, contrast, high-contrast behavior, zoom, pointer targets, filtering, virtualization decision, reducer tests, rendered tests, browser and screen-reader matrix, and known limitations. Fail release if Tab enters every row, focus disappears on collapse, hidden nodes remain reachable, arrow keys scroll the page unexpectedly, selection follows focus without disclosure, state announcements are missing, or a simpler disclosure list would meet the need. Accessibility here is the architecture of interaction, not a final pass that decorates a component after its state model has hardened.

The release decision includes a simpler-control challenge: reviewers must explain why disclosure lists or nested links do not meet the task. That gate keeps the full tree interaction only where hierarchical keyboard exploration has user value, reducing the risk of shipping a technically conforming composite widget where ordinary page navigation would be clearer.

Accessible tree view React code earns trust through stable focus and complete keyboard behavior, not nested divs with ARIA labels. Test the accessible tree view React interaction after filtering, collapse, insertion, and virtualization.