Headless / Specialized

TreeView

TreeView renders a nested, expandable tree. It flattens a recursive node structure, tracks the expanded and selected state, and wires the WAI-ARIA tree pattern with full keyboard navigation.

Loading playground…
Updated 3 days agoEdit on GitHubWeb & native

What it is

A single component over a TreeNode array. It flattens the visible nodes — those whose ancestors are expanded — and hands each one to your renderNode function with its depth and its expanded, selected, and focused flags, plus toggle and select callbacks. Expansion and selection state are managed for you; the markup of each row is yours.

Install

example.tsx
yarn add @usemotif/headless
example.tsx
import { TreeView } from '@usemotif/headless';

API

TreeView

stable
function TreeView<T>(props: TreeViewProps<T>): JSX.Element
datareadonly TreeNode<T>[]required

The root nodes. Each node may carry its own `children`.

renderNode(info) => ReactElementrequired

Renders one node. `info` carries `node`, `depth`, `isExpanded`, `isSelected`, `isFocused`, `toggle`, and `select`.

value / defaultValue / onValueChangestring / string / (next: string) => void

The selected node id, controlled or uncontrolled.

defaultExpandedreadonly string[]= []

Node ids expanded on first render.

aria-labelstring

Names the tree.

styleCSSProperties

Inline style for the tree container.

TreeNode

example.tsx
interface TreeNode<T = unknown> {
id: string;
label: ReactNode;
data?: T;                            // your own payload
children?: readonly TreeNode<T>[];   // presence makes it a branch
disabled?: boolean;
}

Keyboard

KeyAction
Arrow Down / UpMove focus to the next / previous visible node
Arrow RightExpand a collapsed branch
Arrow LeftCollapse an expanded branch
Enter / SpaceSelect the focused node

Accessibility

TreeView implements the WAI-ARIA tree pattern. The container is role="tree"; each row is role="treeitem" with aria-level for its depth, aria-expanded on branches, aria-selected, and aria-disabled where set. A screen reader announces the node's depth, whether it is open, and whether it is selected.

Indentation is the visual cue for depth — but aria-level is the one assistive technology reads. Both come from the depth TreeView gives renderNode; use it for the visual indent and trust the component to set the ARIA.

Examples

A file tree:

example.tsx
<TreeView
data={fileTree}
defaultExpanded={['src']}
onValueChange={openFile}
aria-label="Project files"
renderNode={({ node, depth, isExpanded, isSelected, toggle, select }) => (
  <HStack
    gap="$1"
    pl={depth * 16}
    bg={isSelected ? '$colors.surface.muted' : undefined}
    onPress={() => {
      if (node.children) toggle();
      else select();
    }}
  >
    {node.children ? <Text>{isExpanded ? '▾' : '▸'}</Text> : null}
    <Text>{node.label}</Text>
  </HStack>
)}
/>