Treeview: roving focus + expand/collapse (keyboard + ARIA)
A treeview (like a file explorer sidebar) is a classic keyboard pattern. Many implementations look fine visually but fail for keyboard and assistive tech because they don’t expose structure, state, or predictable navigation.
Live demo (no tracking):
When to use a treeview
- Use a treeview when you’re representing a hierarchy (folders → subfolders → items).
- If the structure is shallow and users switch views frequently, tabs might be simpler.
- If users mainly read long content, an accordion may be better.
Semantic requirements (core ARIA roles)
- Container:
role="tree"with an accessible name (aria-label). - Each visible node:
role="treeitem". - Children container:
role="group"immediately after the parent treeitem (or nested). - Expandable nodes:
aria-expanded="true/false"on the treeitem.
In a full implementation you may also manage selection state (aria-selected) and multi-select behavior, but start with structure + expand/collapse + navigation.
Keyboard navigation (expected behaviors)
- Tab enters the tree (only one treeitem is a Tab stop).
- ↑/↓ move between visible items.
- → expands a collapsed folder; if already expanded, moves to the first child.
- ← collapses an expanded folder; otherwise moves to the parent item.
- Home/End jump to first/last visible item.
- Enter “opens” the item (often the same as click).
Roving tabindex (the key to a sane Tab order)
Don’t put every node in the page tab order. Instead, use roving tabindex:
- Exactly one treeitem has
tabindex="0"(the current focusable item). - All other treeitems have
tabindex="-1". - Arrow keys move focus and update the roving tabindex target.
Predictable focus on collapse
Collapsing a node should keep focus on the node you collapsed. Avoid teleporting focus to unrelated items or removing the focused element from the DOM while it still has focus.
Minimal markup skeleton
<div role="tree" aria-label="Files">
<div role="treeitem" id="ti-src" aria-expanded="true" tabindex="0">src</div>
<div role="group" aria-label="src children">
<div role="treeitem" id="ti-app" tabindex="-1">App.jsx</div>
</div>
</div>
You still need JS to (1) manage roving tabindex, (2) expand/collapse groups, and (3) implement the keyboard interactions above.