Skip to Content
⭐️ Leave a star →
ComponentsTreeView

Tree View

Recursive tree with expandable branches and optional single / multiple selection. You pass nodes (a recursive TreeNode[]) and a renderItem render-prop — Tree View handles expand/collapse, selection state, ARIA roles, and arrow-key navigation.

Features

Recursive node structure — branches and leaves use the same shape.
Selection modes: none, single, multiple.
Controlled and uncontrolled expanded set and selected set.
Per-row render-prop with level, expanded, selected, hasChildren, toggle(), select().
Full keyboard navigation: arrows, Home/End, Enter/Space.
Proper ARIA: role="tree", role="treeitem", aria-expanded, aria-selected, aria-level.

Example

src
components
Button.tsx✓
Card.tsx
index.ts
package.json
README.md
import { TreeView } from '@wire-ui/react' import type { TreeNode } from '@wire-ui/react' const nodes: TreeNode[] = [ { id: 'src', label: 'src', children: [ { id: 'src/index.ts', label: 'index.ts' }, { id: 'src/app.tsx', label: 'app.tsx' }, ], }, { id: 'README.md', label: 'README.md' }, ] <TreeView.Root nodes={nodes} defaultExpanded={['src']} selectionMode="single" renderItem={(node, state) => ( <div onClick={state.select} style={{ paddingLeft: state.level * 16 }}> {state.hasChildren && ( <button data-tree-toggle onClick={(e) => { e.stopPropagation(); state.toggle() }}> {state.expanded ? '▾' : '▸'} </button> )} <span>{node.label}</span> </div> )} />

Styling

Each visible row is a <div role="treeitem"> with data-state, data-selected, data-disabled, data-level, and data-has-children. Indent rows using state.level from the render-prop (it’s 1-based for top-level nodes).

<div className=" data-[state=open]:font-medium data-[selected]:bg-[#f5f5f5] data-[selected]:font-medium data-[disabled]:opacity-50 data-[disabled]:cursor-not-allowed " />

Important: nest your toggle button under data-tree-toggle and stop propagation in its click handler. The Tree’s internal click handler ignores nested clicks unless they originate from a [data-tree-toggle] element — this lets row-clicks select and chevron-clicks toggle independently.

Using data attributes

[role="treeitem"][data-state="open"] { /* expanded branch */ } [role="treeitem"][data-state="closed"] { /* collapsed branch */ } [role="treeitem"][data-selected] { /* selected row */ } [role="treeitem"][data-disabled] { opacity: 0.5; } [role="treeitem"][data-has-children] { /* branch, not leaf */ }

TreeNode shape

FieldTypeDescription
idstringStable unique id (used for expanded / selected sets)
labelReactNodeDefault content passed to your render-prop
childrenTreeNode[]Child nodes; absent or empty = leaf
disabledbooleanDisable selection and keyboard activation for this node
dataunknownArbitrary payload — read it in the render-prop via node.data

Root props

PropTypeDefaultDescription
nodesTreeNode[]requiredTree data
expandedstring[]—Controlled expanded ids
defaultExpandedstring[][]Initial expanded ids
onExpandedChange(expanded: string[]) => void—Called when expansion changes
selectionMode'none' | 'single' | 'multiple''none'Selection behaviour
selectedstring[]—Controlled selected ids
defaultSelectedstring[][]Initial selected ids
onSelectionChange(selected: string[]) => void—Called when selection changes
renderItem(node: TreeNode, state: TreeItemState) => ReactNoderequiredRender each visible row

TreeItemState (passed to renderItem)

FieldTypeDescription
levelnumber1-based depth (1 = top-level)
expandedbooleanTrue when the branch is open
selectedbooleanTrue when the node is in the selected set
hasChildrenbooleanTrue when the node has at least one child
disabledbooleanTrue when node.disabled is set
toggle() => voidToggle this branch’s expanded state
select() => voidSelect / deselect this node (respects selectionMode)

Data attributes

AttributeValues
data-state"open" / "closed" (only meaningful on branches)
data-selectedPresent when selected
data-disabledPresent when disabled
data-level1, 2, 3, …
data-has-childrenPresent on branch nodes

Accessibility

  • Root is role="tree". When selectionMode === 'multiple' it also has aria-multiselectable="true".
  • Each row is role="treeitem" with aria-level, aria-expanded (for branches), aria-selected, and aria-disabled.
  • Child groups use role="group" to scope screen-reader navigation.
  • Rows are focusable (tabIndex=0) so users can Tab into the tree, then use arrow keys to traverse.

Keyboard Interactions

KeyDescription
↓Move focus to the next visible row.
↑Move focus to the previous visible row.
→Expand the focused branch, or move to its first child if already open.
←Collapse the focused branch, or move to its parent if already a leaf.
Enter / SpaceToggle selection on the focused row.
TabMove focus in/out of the tree.
Last updated on

MIT License © 2026 wire-ui

Tree View – Wire UI