Skip to content

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.

On this page

Example

src
components
index.ts
README.md

Selected: README.md

Tree.tsxtsx
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.

  1. Tree

    Hierarchy container and the tree's single tab stop; arrow keys walk visible rows. Owns a DOM element.

  2. TreeGroupOptional

    Container for one item's children; it gets the hidden attribute while its parent is collapsed. Owns a DOM element.

  3. TreeItem

    Selectable row; nesting a TreeGroup inside makes it an expandable branch. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Wrap top-level TreeItems in Tree and give it an aria-label.

  2. 2

    Add the supporting parts

    Nest a TreeGroup of child TreeItems inside an item to make it an expandable branch.

  3. 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

Importtsx
import { Tree, TreeGroup, TreeItem } from "@comp0/react";

Tree

DOM element

Hierarchy container and the tree's single tab stop; arrow keys walk visible rows.

PropTypeDescription
aria-labelstringNames the tree; nothing labels it automatically.
valuestringControlled selected item.
defaultValuestringInitial selected item.
onChange(value: string) => voidReceives the next selected item's value.
expanded / defaultExpandedstring[]Controlled or initial expanded items.
onExpandedChange(expanded: string[]) => voidReceives the next expanded item values.

TreeGroup

OptionalDOM element

Container for one item's children; it gets the hidden attribute while its parent is collapsed.

PropTypeDescription

TreeItem

DOM element

Selectable row; nesting a TreeGroup inside makes it an expandable branch.

PropTypeDescription
valuestringThis item's selection and expansion key.
disabledbooleanDisables the item and removes it from the arrow-key order.
textValuestringOverrides 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-visible

on TreeItem

The item has visible keyboard focus.

Keep exploring