Navigation
Tree
A nested list where branches expand and collapse behind one tab stop.
When to use it: Use it for hierarchies such as file explorers, categories, or document outlines.
Example
Selected: README.md
import { useState } from "react";
import { Tree, TreeGroup, TreeItem } from "@comp0/react";
import { DocumentIcon, FolderIcon, FolderOpenIcon } from "@heroicons/react/16/solid";
type FileNode = {
name: string;
children?: FileNode[];
};
const files: FileNode[] = [
{
name: "src",
children: [
{
name: "components",
children: [{ name: "Button.tsx" }, { name: "Input.tsx" }],
},
{ name: "index.ts" },
],
},
{ name: "README.md" },
];
function FileItem({ node, expanded }: { node: FileNode; expanded: string[] }) {
let Glyph = DocumentIcon;
if (node.children) {
Glyph = FolderIcon;
if (expanded.includes(node.name)) Glyph = FolderOpenIcon;
}
return (
<TreeItem value={node.name} textValue={node.name} className="grid gap-0.5 outline-none">
<span className="flex cursor-pointer items-center gap-1.5 rounded px-2 py-1.5 text-base text-zinc-800 sm:py-1 sm:text-sm dark:text-zinc-100 [[data-selected]>&]:bg-teal-100 [[data-selected]>&]:text-teal-950 dark:[[data-selected]>&]:bg-teal-950 dark:[[data-selected]>&]:text-teal-50 [[role=treeitem]:focus-visible>&]:outline-2 [[role=treeitem]:focus-visible>&]:outline-teal-600 dark:[[role=treeitem]:focus-visible>&]:outline-teal-400">
<Glyph className="size-4 shrink-0 text-zinc-400 dark:text-zinc-500" aria-hidden="true" />
{node.name}
</span>
{node.children && (
<TreeGroup className="grid gap-0.5 pl-5">
{node.children.map((child) => (
<FileItem key={child.name} node={child} expanded={expanded} />
))}
</TreeGroup>
)}
</TreeItem>
);
}
export function Example() {
const [selected, setSelected] = useState("README.md");
const [expanded, setExpanded] = useState(["src"]);
return (
<div className="flex max-w-xs flex-col gap-2">
<Tree
aria-label="Project files"
className="grid gap-0.5 rounded border border-zinc-950/10 p-1 dark:border-white/10"
value={selected}
onChange={setSelected}
expanded={expanded}
onExpandedChange={setExpanded}
>
{files.map((node) => (
<FileItem key={node.name} node={node} expanded={expanded} />
))}
</Tree>
<p className="text-base text-zinc-600 sm:text-sm dark:text-zinc-400">Selected: {selected}</p>
</div>
);
}Anatomy
Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.
A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.
Tree
Hierarchy container and the tree's single tab stop; arrow keys walk visible rows. Owns a DOM element.
TreeGroupOptional
Container for one item's children; it gets the hidden attribute while its parent is collapsed. Owns a DOM element.
TreeItem
Selectable row; nesting a TreeGroup inside makes it an expandable branch. Owns a DOM element.
Step by step
- 1
Add the main part
Wrap top-level TreeItems in Tree and give it an aria-label.
- 2
Add the supporting parts
Nest a TreeGroup of child TreeItems inside an item to make it an expandable branch.
- 3
Make the behavior clear
Read the selection from onChange; pass defaultExpanded, or expanded with onExpandedChange, to manage the open branches. Clicking an expandable row selects it and toggles its branch.
Exampletsx <Tree aria-label="Files" defaultExpanded={["src"]}><TreeItem value="src">src<TreeGroup><TreeItem value="index">index.ts</TreeItem></TreeGroup></TreeItem></Tree>
Keyboard
- ⇥
- Moves into the tree to the active item; Tab again leaves.
- ↓
- Moves to the next visible item without wrapping.
- ↑
- Moves to the previous visible item without wrapping.
- →
- Expands a collapsed item; on an expanded item, moves to its first child.
- ←
- Collapses an expanded item; otherwise moves to the parent item.
- Home
- Moves to the first visible item.
- End
- Moves to the last visible item.
- ↵
- Selects the focused item.
- Space
- Selects the focused item.
Forms and accessibility
Selection does not create a native form value; mirror it into a hidden input when a form needs it.
Accessibility checklist
- Give the tree an aria-label that names the hierarchy, such as Project files.
- Levels, positions, and expansion state are announced automatically; keep each row's visible text meaningful on its own and pass textValue when markup obscures it.
- Clicking an expandable row both selects it and toggles its branch, while ArrowRight and ArrowLeft expand and collapse for keyboard users; avoid extra click targets inside rows.
API reference
import { Tree, TreeGroup, TreeItem } from "@comp0/react";Tree
DOM elementHierarchy container and the tree's single tab stop; arrow keys walk visible rows.
| Prop | Type | Description |
|---|---|---|
aria-label | string | Names the tree; nothing labels it automatically. |
value | string | Controlled selected item. |
defaultValue | string | Initial selected item. |
onChange | (value: string) => void | Receives the next selected item's value. |
expanded / defaultExpanded | string[] | Controlled or initial expanded items. |
onExpandedChange | (expanded: string[]) => void | Receives the next expanded item values. |
TreeGroup
OptionalDOM elementContainer for one item's children; it gets the hidden attribute while its parent is collapsed.
| Prop | Type | Description |
|---|
TreeItem
DOM elementSelectable row; nesting a TreeGroup inside makes it an expandable branch.
| Prop | Type | Description |
|---|---|---|
value | string | This item's selection and expansion key. |
disabled | boolean | Disables the item and removes it from the arrow-key order. |
textValue | string | Overrides the text crawled from the row for typeahead when markup makes it ambiguous. |
Style hooks
Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.
[data-selected]on TreeItem
- The item is selected.
[data-expanded]on TreeItem
- The item's branch is open.
[data-disabled]on TreeItem
- The item is disabled.
:focus-visibleon TreeItem
- The item has visible keyboard focus.