Data Display
Tree View
AvailableDisplays a hierarchical structure. Supports expand/collapse and navigation similar to a folder tree.
What Is a Tree View?
A tree view is a UI that displayshierarchical structure with collapsible branches, much like folders and files. It's commonly used in file explorers, sidebar menus, org charts, and category lists.
The key aspects are "parent-child nesting (hierarchy)" and "expandable/collapsible branches." Visually, indentation and triangle markers convey structure, but for users who can't see the screen, you need to communicate"which level they're on, which item out of how many, and whether it's expanded or collapsed". That's the heart of accessibility here.
Why Does Accessibility Matter?
Building a tree from visual styling alone leaves these people behind:
- Keyboard-only users. If each row is just a
<div>or<li>, it can't be focused with Tab, and branches can't be opened. - Screen reader users. A plain nested list is announced as "list item" only — hierarchy depth, expand/collapse state, and position within the set are completely lost.
The solution is to use role="tree" on the container, role="treeitem" on each row,role="group" on child collections, and aria-expanded on expandable branches. Additionally, the entire tree should be a single Tab stop (roving tabindex), with arrow keys handling movement within.
Live Demo (Recommended Implementation)
The file tree below follows the APG specification. Try navigating it without using the mouse, using only the keyboard.
- 📁 Documents
- 📁 Projects
- 📄 plan.md
- 📄 minutes.md
- 📁 Images
- 🖼️ logo.png
- 🖼️ diagram.svg
- 📄 resume.pdf
- 📁 Projects
- 📁 Downloads
- 📦 setup.zip
Try it: Tab once to enter the tree → ↓ ↑ to move between items → → to expand a branch / move to child, ← to collapse / move to parent → Home / End to jump to first / last.
Tip
Turn on a screen reader (on macOS, press ⌘+F5 to toggle VoiceOver) and focus an item. You'll hear something like "Documents, expanded, 1 of 2, level 1" —name, expand/collapse state, position, and hierarchy level. This is the effect of role="tree" and aria-expanded.
Keyboard Interaction
| Key | Action | Priority |
|---|---|---|
| Tab | Enter / leave the tree (only one tab stop for the entire tree) | Required |
| ↓ / ↑ | Move focus to the next / previous visible item | Required |
| → | Expand a closed branch. Move to first child if open. No action on a leaf node | Required |
| ← | Collapse an open branch. Otherwise move to the parent item | Required |
| Home / End | Move to the first / last visible item | Recommended |
Note
A tree follows the "one Tab stop for the entire tree" principle. Only one item has tabindex="0" (making it the focusable target), while all others have tabindex="-1". Arrow keys switch which item is focusable. This mechanism is called roving tabindex.
Required WAI-ARIA Roles & Properties
| Target | Attribute / Role | Meaning |
|---|---|---|
| Container | role="tree" + aria-label | The entire hierarchical list. Also provide a label (name). |
| Each row | role="treeitem" | A single item in the tree (folder/file). |
| Expandable branch | aria-expanded="true | false" | Whether children are expanded. Must be updated on toggle. Do not apply to leaf nodes. |
| Group of children | role="group" | A container for the child treeitems of a parent item. |
| Each row (optional) | aria-level / aria-setsize / aria-posinset | Depth level, total count of siblings, and position within siblings. Inferred from nesting but can be explicit. |
| Each row | tabindex="0 | -1" | Only the focused item gets 0; all others get -1 (roving tabindex). |
Recommended Pattern (Good)
Good / Recommended
Use tree / treeitem / group to represent hierarchy, and synchronize aria-expanded and roving tabindex with JavaScript.
Markup:
<ul role="tree" aria-label="Files">
<li role="treeitem" aria-expanded="true" tabindex="0">
<span class="tree-label">📁 Documents</span>
<ul role="group">
<li role="treeitem" aria-expanded="false" tabindex="-1">
<span class="tree-label">📁 Projects</span>
<ul role="group">
<li role="treeitem" tabindex="-1">
<span class="tree-label">📄 plan.md</span>
</li>
<li role="treeitem" tabindex="-1">
<span class="tree-label">📄 minutes.md</span>
</li>
</ul>
</li>
<li role="treeitem" tabindex="-1">
<span class="tree-label">📄 resume.pdf</span>
</li>
</ul>
</li>
</ul>Keyboard interaction and state synchronization script:
const tree = document.querySelector('[role="tree"]');
// Collect only visible treeitems (those whose ancestors are all expanded)
function visibleItems() {
return Array.from(tree.querySelectorAll('[role="treeitem"]'))
.filter((el) => !el.closest('[role="group"][hidden]'));
}
function isParent(el) {
return el.hasAttribute('aria-expanded');
}
function moveTo(el) {
// Roving tabindex: only the focused item gets 0, others get -1
visibleItems().forEach((i) => i.setAttribute('tabindex', '-1'));
el.setAttribute('tabindex', '0');
el.focus();
}
function setExpanded(el, open) {
el.setAttribute('aria-expanded', String(open));
// Toggle the direct child group (leave grandchildren as-is)
const group = el.querySelector(':scope > [role="group"]');
if (group) group.hidden = !open;
}
tree.addEventListener('keydown', (e) => {
const cur = e.target.closest('[role="treeitem"]');
if (!cur) return;
const items = visibleItems();
const i = items.indexOf(cur);
switch (e.key) {
case 'ArrowDown':
if (i < items.length - 1) moveTo(items[i + 1]);
break;
case 'ArrowUp':
if (i > 0) moveTo(items[i - 1]);
break;
case 'ArrowRight':
if (isParent(cur) && cur.getAttribute('aria-expanded') === 'false') {
setExpanded(cur, true); // Closed parent → expand
} else if (isParent(cur)) {
const child = cur.querySelector(':scope > [role="group"] [role="treeitem"]');
if (child) moveTo(child); // Open parent → move to first child
}
break;
case 'ArrowLeft':
if (isParent(cur) && cur.getAttribute('aria-expanded') === 'true') {
setExpanded(cur, false); // Open parent → collapse
} else {
const parentGroup = cur.parentElement.closest('[role="group"]');
const parent = parentGroup &&
parentGroup.closest('[role="treeitem"]');
if (parent) moveTo(parent); // Otherwise → move to parent
}
break;
case 'Home':
moveTo(items[0]);
break;
case 'End':
moveTo(items[items.length - 1]);
break;
default:
return;
}
e.preventDefault();
});Note
Arrow key "movement" targets only visible items (those whose ancestors are all expanded). Items inside a collapsed branch are within a hidden group, so they must be excluded from the navigation candidates.
Anti-Pattern (Bad)
Below is a "visual-only tree" built with <ul>/<li> and onclick.It works with a mouse but is completely inoperable via keyboard.Compare the difference with the demo above.
- 📁 Documents
- 📁 Projects
- 📄 plan.md
- 📄 minutes.md
- 📄 resume.pdf
- 📁 Projects
Try it: Pressing Tab doesn't focus any item. A screen reader announces it as 'just a list' — hierarchy, expand/collapse state, and position are completely missing.
<!-- ❌ Anti-pattern -->
<!-- Just a nested list. No role or aria-expanded -->
<ul class="bad-tree">
<li>
<span onclick="toggle(this)">📁 Documents</span>
<ul>
<li>
<span onclick="toggle(this)">📁 Projects</span>
<ul style="display:none">
<li><span>📄 plan.md</span></li>
</ul>
</li>
<li><span>📄 resume.pdf</span></li>
</ul>
</li>
</ul>Bad / Avoid
Problems with this implementation:
- Not keyboard operable —
span/lielements can't receive focus, and there's no arrow key navigation. - Not recognized as a tree — Without
role="tree", screen readers announce it as "just a list." - Expand/collapse state not conveyed — Without
aria-expanded, users can't tell if a branch is open or closed. - Hierarchy and position not conveyed — The current level and position within the set are not announced.
Tip
Using nested <ul>/<li> as a base is fine. Add role="treeitem" to the li elements, role="tree" to the outer container,role="group" to child ul elements, then add aria-expanded, roving tabindex, and arrow key handling — and you get an accessible tree with the same visual appearance.
Implementation Checklist
- The container has role="tree" and aria-label (name)
- Each row has role="treeitem"; groups of children have role="group"
- Expandable branches have aria-expanded that always matches their open/closed state
- Only one tab stop for the entire tree (roving tabindex: only one tabindex is 0)
- Up/Down arrows move between visible items; Right expands/enters child; Left collapses/moves to parent
- (Optional) Home/End move to the first and last items
- Items inside collapsed branches are hidden from keyboard and screen readers (hidden)
- Operable by keyboard alone with visible focus (focus ring is not removed)
Source (English):Tree View Pattern — W3C APG(opens in a new tab)