treeitem
An item in a tree hierarchy. Tree items can be nested within other tree items and may be expandable to reveal children. Used in file browsers, navigation menus, and hierarchical data displays.
Overview
The treeitem role represents an item within a tree widget. Tree items can be either leaf nodes (no children) or branch nodes (expandable with children). They support selection, expansion, and complex keyboard navigation.
Tree items with children use aria-expanded to indicate whether they are expanded or collapsed. Nested items are wrapped in a role="group" container.
Tree Structure
A tree consists of: role="tree" (the container), role="treeitem" (each node), and role="group" (wrapper for child nodes). Only one item should have tabindex="0" at a time.
Live Demo: File Tree Explorer
Interactive File Tree
- ๐ Documents
- ๐ Resume.pdf
- ๐ Cover Letter.docx
- ๐ Projects
- ๐ Project A
- ๐ Project B
- ๐ Images
- ๐ Notes.txt
Selected: documents
Keyboard support: Use โ โ to navigate, โ to expand, โ to collapse, Enter to select.
Code Examples
Basic Tree Structure
<!-- Basic Tree Structure -->
<ul role="tree" aria-label="File explorer">
<li role="treeitem" aria-expanded="true" aria-selected="false">
Documents
<ul role="group">
<li role="treeitem" aria-selected="false">Resume.pdf</li>
<li role="treeitem" aria-selected="true">Cover Letter.docx</li>
</ul>
</li>
<li role="treeitem" aria-expanded="false" aria-selected="false">
Images
<ul role="group">
<li role="treeitem" aria-selected="false">Photo1.jpg</li>
<li role="treeitem" aria-selected="false">Photo2.png</li>
</ul>
</li>
</ul>
<!-- Key attributes:
- role="treeitem" on each tree node
- aria-expanded on parent nodes (true/false)
- aria-selected on the focused/selected node
- role="group" on nested <ul> containers -->Keyboard Navigation
<!-- Tree with Full Keyboard Support -->
<ul role="tree" aria-label="Project files" id="file-tree">
<li role="treeitem" aria-expanded="true" id="node-1" tabindex="0">
src
<ul role="group">
<li role="treeitem" id="node-2" tabindex="-1">index.js</li>
<li role="treeitem" aria-expanded="false" id="node-3" tabindex="-1">
components
<ul role="group">
<li role="treeitem" id="node-4" tabindex="-1">Button.js</li>
<li role="treeitem" id="node-5" tabindex="-1">Card.js</li>
</ul>
</li>
</ul>
</li>
</ul>
<script>
const tree = document.getElementById('file-tree');
const items = tree.querySelectorAll('[role="treeitem"]');
let currentIndex = 0;
tree.addEventListener('keydown', (e) => {
const currentItem = items[currentIndex];
const isExpanded = currentItem.getAttribute('aria-expanded') === 'true';
const hasChildren = currentItem.querySelector('[role="group"]');
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
moveFocus(1);
break;
case 'ArrowUp':
e.preventDefault();
moveFocus(-1);
break;
case 'ArrowRight':
e.preventDefault();
if (hasChildren && !isExpanded) {
expandNode(currentItem);
} else if (hasChildren && isExpanded) {
// Move to first child
moveFocus(1);
}
break;
case 'ArrowLeft':
e.preventDefault();
if (hasChildren && isExpanded) {
collapseNode(currentItem);
} else {
// Move to parent
moveToParent(currentItem);
}
break;
case 'Home':
e.preventDefault();
currentIndex = 0;
updateFocus();
break;
case 'End':
e.preventDefault();
currentIndex = getVisibleItems().length - 1;
updateFocus();
break;
case 'Enter':
case ' ':
e.preventDefault();
if (hasChildren) {
toggleNode(currentItem);
} else {
activateNode(currentItem);
}
break;
}
});
function moveFocus(direction) {
const visible = getVisibleItems();
const newIndex = currentIndex + direction;
if (newIndex >= 0 && newIndex < visible.length) {
currentIndex = newIndex;
updateFocus();
}
}
function updateFocus() {
items.forEach(item => item.setAttribute('tabindex', '-1'));
const visible = getVisibleItems();
visible[currentIndex].setAttribute('tabindex', '0');
visible[currentIndex].focus();
}
function getVisibleItems() {
return [...items].filter(item => {
let parent = item.parentElement.closest('[role="treeitem"]');
while (parent) {
if (parent.getAttribute('aria-expanded') === 'false') {
return false;
}
parent = parent.parentElement.closest('[role="treeitem"]');
}
return true;
});
}
</script>Selection Modes
<!-- Tree with Selection Modes -->
<!-- Single Selection Tree -->
<ul role="tree" aria-label="Single select" aria-multiselectable="false">
<li role="treeitem" aria-selected="true" tabindex="0">
Selected Item
</li>
<li role="treeitem" aria-selected="false" tabindex="-1">
Unselected Item
</li>
</ul>
<!-- Multi-Selection Tree -->
<ul role="tree" aria-label="Multi select" aria-multiselectable="true">
<li role="treeitem" aria-selected="true" tabindex="0">
โ Selected Item 1
</li>
<li role="treeitem" aria-selected="true" tabindex="-1">
โ Selected Item 2
</li>
<li role="treeitem" aria-selected="false" tabindex="-1">
โ Unselected Item
</li>
</ul>
<!-- For multi-select trees:
- Ctrl+Click toggles selection
- Shift+Click selects range
- Ctrl+A selects all
- aria-multiselectable="true" on the tree -->React Component
// React Tree Component
import { useState, useCallback, useRef, useEffect } from 'react';
interface TreeNode {
id: string;
label: string;
children?: TreeNode[];
}
interface TreeProps {
data: TreeNode[];
label: string;
onSelect?: (nodeId: string) => void;
}
function Tree({ data, label, onSelect }: TreeProps) {
const [expanded, setExpanded] = useState<Set<string>>(new Set());
const [selected, setSelected] = useState<string | null>(null);
const treeRef = useRef<HTMLUListElement>(null);
const toggleExpanded = useCallback((nodeId: string) => {
setExpanded(prev => {
const next = new Set(prev);
if (next.has(nodeId)) {
next.delete(nodeId);
} else {
next.add(nodeId);
}
return next;
});
}, []);
const handleSelect = useCallback((nodeId: string) => {
setSelected(nodeId);
onSelect?.(nodeId);
}, [onSelect]);
const handleKeyDown = useCallback((
e: React.KeyboardEvent,
node: TreeNode,
level: number
) => {
const hasChildren = node.children && node.children.length > 0;
const isExpanded = expanded.has(node.id);
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
if (hasChildren && !isExpanded) {
toggleExpanded(node.id);
}
break;
case 'ArrowLeft':
e.preventDefault();
if (hasChildren && isExpanded) {
toggleExpanded(node.id);
}
break;
case 'Enter':
case ' ':
e.preventDefault();
if (hasChildren) {
toggleExpanded(node.id);
}
handleSelect(node.id);
break;
}
}, [expanded, toggleExpanded, handleSelect]);
const renderNode = (node: TreeNode, level: number = 1) => {
const hasChildren = node.children && node.children.length > 0;
const isExpanded = expanded.has(node.id);
const isSelected = selected === node.id;
return (
<li
key={node.id}
role="treeitem"
aria-expanded={hasChildren ? isExpanded : undefined}
aria-selected={isSelected}
aria-level={level}
tabIndex={isSelected ? 0 : -1}
onClick={(e) => {
e.stopPropagation();
handleSelect(node.id);
}}
onKeyDown={(e) => handleKeyDown(e, node, level)}
className="tree-item"
>
<div className="tree-item-content" style={{ paddingLeft: level * 20 }}>
{hasChildren && (
<button
onClick={(e) => {
e.stopPropagation();
toggleExpanded(node.id);
}}
aria-hidden="true"
tabIndex={-1}
className="expand-button"
>
{isExpanded ? 'โผ' : 'โถ'}
</button>
)}
<span>{hasChildren ? '๐' : '๐'} {node.label}</span>
</div>
{hasChildren && isExpanded && (
<ul role="group">
{node.children!.map(child => renderNode(child, level + 1))}
</ul>
)}
</li>
);
};
return (
<ul
ref={treeRef}
role="tree"
aria-label={label}
className="tree"
>
{data.map(node => renderNode(node))}
</ul>
);
}
// Usage
const fileData = [
{
id: 'src',
label: 'src',
children: [
{ id: 'index', label: 'index.js' },
{
id: 'components',
label: 'components',
children: [
{ id: 'button', label: 'Button.tsx' },
{ id: 'card', label: 'Card.tsx' },
],
},
],
},
{ id: 'readme', label: 'README.md' },
];
<Tree
data={fileData}
label="Project files"
onSelect={(id) => console.log('Selected:', id)}
/>Drag and Drop
<!-- Tree with Drag and Drop -->
<ul role="tree" aria-label="Draggable tree">
<li
role="treeitem"
aria-grabbed="false"
draggable="true"
id="drag-item-1"
>
Draggable Item 1
</li>
<li
role="treeitem"
aria-grabbed="true"
draggable="true"
id="drag-item-2"
>
Currently Grabbed
</li>
<li
role="treeitem"
aria-dropeffect="move"
id="drop-target"
>
Drop Target
</li>
</ul>
<script>
// Drag start
item.addEventListener('dragstart', (e) => {
item.setAttribute('aria-grabbed', 'true');
// Announce to screen readers
announce('Grabbed ' + item.textContent);
});
// Drag end
item.addEventListener('dragend', (e) => {
item.setAttribute('aria-grabbed', 'false');
announce('Dropped ' + item.textContent);
});
// For drop targets
target.addEventListener('dragover', (e) => {
e.preventDefault();
target.setAttribute('aria-dropeffect', 'move');
});
</script>
<!-- Note: Drag and drop requires significant
additional keyboard support for accessibility -->Checkbox Tree (Tri-state)
<!-- Tree with Checkboxes (Tri-state) -->
<ul role="tree" aria-label="Permissions">
<li role="treeitem" aria-expanded="true">
<span role="checkbox" aria-checked="mixed" tabindex="0">
โ All Permissions
</span>
<ul role="group">
<li role="treeitem">
<span role="checkbox" aria-checked="true" tabindex="-1">
โ Read
</span>
</li>
<li role="treeitem">
<span role="checkbox" aria-checked="true" tabindex="-1">
โ Write
</span>
</li>
<li role="treeitem">
<span role="checkbox" aria-checked="false" tabindex="-1">
โ Delete
</span>
</li>
</ul>
</li>
</ul>
<!-- Tri-state checkbox values:
- aria-checked="true" - all descendants selected
- aria-checked="false" - no descendants selected
- aria-checked="mixed" - some descendants selected -->Keyboard Support
Skips collapsed children
Skips collapsed children
If expanded, moves to first child
If collapsed or leaf, moves to parent
In the entire tree
Last visible item in tree
Behavior depends on item type
Asterisk key
Best Practices
Use aria-expanded on parent nodes to indicate expand/collapse state
Wrap child nodes in role="group" for proper structure
Implement roving tabindex - only focused item has tabindex="0"
Use aria-level to indicate nesting depth if not implicit
Provide visual indicators for expandable vs leaf nodes
Support type-ahead for jumping to items by first letter
Don't omit role="group" around nested treeitems
Don't forget to manage focus when nodes collapse
Don't use trees for simple navigation - use a menu or links
Don't nest trees within trees
Supported ARIA Attributes
aria-expandedWhether the node is expanded (branch nodes only)
aria-selectedWhether the node is selected
aria-levelNesting level (1-based, can be implicit from DOM)
aria-setsizeTotal number of siblings
aria-posinsetPosition within siblings (1-based)
aria-labelAccessible name for the treeitem
aria-grabbedFor drag-and-drop trees
aria-checkedFor checkbox trees (true/false/mixed)
Common Use Cases
Accessibility Notes
Focus Management
Use roving tabindex: only the currently focused item has tabindex="0", all others have tabindex="-1". When a node collapses, move focus to the collapsed parent if a child was focused.
Screen Reader Behavior
Screen readers announce treeitems with their label, level, position (e.g., "item 2 of 5"), and expanded state. Use aria-level, aria-setsize, and aria-posinset when nesting isn't implicit from the DOM.
Type-Ahead Navigation
Consider implementing type-ahead: when users type characters, focus moves to the next item starting with those characters. This helps users quickly navigate large trees.

