Overview
A tree view presents hierarchical data as an expandable and collapsible structure. The most common mistake is building trees from nested <ul> lists with click handlers on generic elements. These miss the required ARIA tree roles, expanded/collapsed states, level information, and keyboard navigation that screen reader and keyboard users depend on.
WCAG Criteria:
- 1.3.1 Info and Relationships — the hierarchy and nesting levels must be programmatically conveyed through ARIA roles and properties
- 2.1.1 Keyboard — arrow keys must navigate the tree, expand/collapse nodes, and move between levels
- 4.1.2 Name, Role, Value — each item must expose its role (
treeitem), expanded state, level, and position
Key requirements:
- The container needs
role="tree"with an accessible label - Each item needs
role="treeitem"witharia-level,aria-setsize, andaria-posinset - Parent nodes need
aria-expanded="true"or"false"— leaf nodes must NOT havearia-expanded - Child groups are wrapped in an element with
role="group" - Use roving
tabindex— the focused item hastabindex="0", all others havetabindex="-1" - Arrow keys: Up/Down move between visible items, Right expands or moves to first child, Left collapses or moves to parent
- Home/End jump to the first/last visible item in the tree
- Enter/Space activate the current item
File Browser Tree
Accessible Tree View vs. Unsemantic Nested Lists
Inaccessible
<!-- No tree roles, no aria-expanded, no keyboard navigation -->
<div class="file-tree">
<ul>
<li>
<span class="folder" onclick="toggleFolder(this)">
Documents
</span>
<ul style="margin-left: 20px">
<li><span>Resume.pdf</span></li>
<li><span>Cover Letter.docx</span></li>
</ul>
</li>
<li>
<span class="folder" onclick="toggleFolder(this)">
Images
</span>
<ul style="margin-left: 20px; display: none">
<li><span>Photo.jpg</span></li>
</ul>
</li>
</ul>
</div>Live Preview
Files
-
▼📁 Documents
- 📄 Resume.pdf
- 📄 Cover Letter.docx
- ▶📁 Images
Accessible
<ul role="tree" aria-label="File browser">
<li role="treeitem" aria-expanded="true"
aria-level="1" aria-setsize="2" aria-posinset="1"
tabindex="0">
Documents
<ul role="group">
<li role="treeitem"
aria-level="2" aria-setsize="2" aria-posinset="1"
tabindex="-1">
Resume.pdf
</li>
<li role="treeitem"
aria-level="2" aria-setsize="2" aria-posinset="2"
tabindex="-1">
Cover Letter.docx
</li>
</ul>
</li>
<li role="treeitem" aria-expanded="false"
aria-level="1" aria-setsize="2" aria-posinset="2"
tabindex="-1">
Images
<ul role="group" hidden>
<li role="treeitem"
aria-level="2" aria-setsize="1" aria-posinset="1"
tabindex="-1">
Photo.jpg
</li>
</ul>
</li>
</ul>
<script>
// Roving tabindex: focused item tabindex="0", rest "-1"
// ArrowDown/Up: move between visible treeitems
// ArrowRight: expand closed node or move to first child
// ArrowLeft: collapse open node or move to parent
// Home/End: first/last visible item
// Enter/Space: activate (toggle expand/collapse)
</script>Live Preview
Files
-
📁 Documents
- 📄 Resume.pdf
- 📄 Cover Letter.docx
- 📁 Images
What’s wrong with the unsemantic nested lists?
- No
role="tree"on the container — screen readers cannot identify the widget as a tree - List items have no
role="treeitem"— their hierarchical role is invisible to assistive technology - No
aria-expandedon folders — users cannot tell whether a node is open or closed - No
aria-level,aria-setsize, oraria-posinset— the nesting depth and position are not conveyed - No keyboard navigation — arrow keys do not work, so keyboard-only users are stranded
- Indentation is purely visual (CSS margin) — the hierarchy is lost for non-visual users
What the screen reader announces:
| Version | Announcement |
|---|---|
| Inaccessible (plain list) | “Documents” — no role, no state, no tree context |
| Accessible (tree) | “Documents, tree item, expanded, level 1, 1 of 2, File browser tree” |
| Collapsed folder | ”Images, tree item, collapsed, level 1, 2 of 2” |
| Leaf item focused | ”Resume.pdf, tree item, level 2, 1 of 2” — no expanded state announced |
| After collapsing | ”Documents, tree item, collapsed, level 1, 1 of 2” |
Key teaching points:
- Roving tabindex, not
aria-activedescendant— roving tabindex (movingtabindex="0"to the active item and setting all others totabindex="-1") provides more reliable VoiceOver support thanaria-activedescendant - Never add
aria-expandedto leaf nodes — only parent items that can be expanded or collapsed should havearia-expanded. Adding it to leaf nodes falsely tells screen readers the item has children - Right/Left arrows for expand/collapse — this is the expected keyboard convention for trees. Right arrow expands a closed node or moves to its first child; Left arrow collapses an open node or moves to its parent