Purpose
Navigate nested structures such as file systems or navigation trees.
Loading ...
treeDisplays a hierarchical set of options or folders. Nodes can be expanded or collapsed to reveal nested treeitems.
Expandable navigation tree with Arrow key interactions and nested groups.
Navigate nested structures such as file systems or navigation trees.
role="tree" contains treeitem elements, optionally grouped inside role="group".
Arrow keys expand/collapse nodes and move between siblings.
List folders and files with nested items.
Provide drill-down navigation for docs or settings.
Enable selecting nested permissions or features.
Each treeitem needs an id, aria-level, aria-expanded (if parent), and optionally aria-selected.
Nested children appear inside role="group" containers positioned after their parent treeitem.
Use aria-labelledby or include text content within the treeitem for naming.
| Action | Keys | Result |
|---|---|---|
| Move between siblings | ArrowUp / ArrowDown | Moves to previous/next visible treeitem. |
| Expand node | ArrowRight | Expands focused node or moves to first child if already expanded. |
| Collapse node | ArrowLeft | Collapses focused node or moves to parent if already collapsed. |
| Jump to edges | Home / End | Moves to the first or last visible node. |
| Typeahead | Character keys | Moves focus to the next item starting with typed characters. |
aria-levelRequired1-based depth of the node. Update dynamically when nodes move.
aria-expandedOptionalOnly on items that own children. Omit when the node is a leaf.
aria-setsizeOptionalTotal number of siblings for a node. Helpful when nodes are virtualized.
aria-posinsetOptionalPosition of the node within its set of siblings (1-based).
aria-selectedOptionalTrue when the node is selected. Useful for multi-select trees.
Maintain roving focus so only one treeitem is tabbable.
Update aria-level/posinset/setsize when dynamically inserting or removing nodes.
Provide visual affordances (chevrons) for expandable nodes and match them with aria-expanded state.
Scroll newly focused nodes into view to avoid hidden focus indicators.
Simple outline showing aria-level and aria-expanded.
<ul role="tree" aria-label="Project files">
<li role="treeitem" aria-expanded="true" aria-level="1" aria-setsize="2" aria-posinset="1">
src
<ul role="group">
<li role="treeitem" aria-level="2" aria-setsize="2" aria-posinset="1">components</li>
<li role="treeitem" aria-level="2" aria-setsize="2" aria-posinset="2">pages</li>
</ul>
</li>
<li role="treeitem" aria-expanded="false" aria-level="1" aria-setsize="2" aria-posinset="2">
tests
</li>
</ul>Scripted keyboard handler for expand/collapse.
const tree = document.querySelector('[role="tree"]')
const items = Array.from(tree.querySelectorAll('[role="treeitem"]'))
function focusItem(index) {
items.forEach((item, i) => (item.tabIndex = i === index ? 0 : -1))
items[index].focus()
}
let active = 0
focusItem(active)
tree.addEventListener('keydown', (event) => {
const current = items[active]
if (event.key === 'ArrowDown') {
event.preventDefault()
active = Math.min(active + 1, items.length - 1)
focusItem(active)
} else if (event.key === 'ArrowUp') {
event.preventDefault()
active = Math.max(active - 1, 0)
focusItem(active)
} else if (event.key === 'ArrowRight') {
if (current.getAttribute('aria-expanded') === 'false') {
current.setAttribute('aria-expanded', 'true')
}
} else if (event.key === 'ArrowLeft') {
if (current.getAttribute('aria-expanded') === 'true') {
current.setAttribute('aria-expanded', 'false')
}
}
})Show the current path outside the tree for context.
If nodes load async, show a spinner and set aria-busy.
Match visual indentation with aria-level values.
aria-level values do not match the visual hierarchy, confusing screen reader output.
Update aria-level whenever nodes are re-parented.
Collapsed nodes are removed from the DOM and lose their ids.
Keep nodes in place and use hidden attribute to collapse.
Individual node inside a tree.
Learn moreContainer wrapping nested children.
Learn moreHybrid role combining tree and grid.
Learn more