Skip to content

Navigation

Tree Grid

A hierarchical table where each row can expand, select, and reveal more columns.

When to use it: Use it for structured hierarchies where columns such as owner, status, or modified date matter alongside the tree.

On this page

Example

NameTypeStatus
srcFolder8 files
componentsFolder4 files
TreeGrid.tsxFileModified
TreeGridRow.tsxFileModified
index.tsFileModified
README.mdFileUntracked

Selected: tree-grid

Tree Grid.tsxtsx
import { useState } from "react";
import {
  TreeGrid,
  TreeGridCell,
  TreeGridColumn,
  TreeGridRow,
  TreeGridRowGroup,
} from "@comp0/react";

type ProjectFile = {
  value: string;
  parentValue?: string;
  name: string;
  kind: "Folder" | "File";
  status: string;
  level: number;
};

const projectFiles: ProjectFile[] = [
  { value: "src", name: "src", kind: "Folder", status: "8 files", level: 0 },
  {
    value: "components",
    parentValue: "src",
    name: "components",
    kind: "Folder",
    status: "4 files",
    level: 1,
  },
  {
    value: "tree-grid",
    parentValue: "components",
    name: "TreeGrid.tsx",
    kind: "File",
    status: "Modified",
    level: 2,
  },
  {
    value: "tree-grid-row",
    parentValue: "components",
    name: "TreeGridRow.tsx",
    kind: "File",
    status: "Modified",
    level: 2,
  },
  {
    value: "index",
    parentValue: "src",
    name: "index.ts",
    kind: "File",
    status: "Modified",
    level: 1,
  },
  { value: "readme", name: "README.md", kind: "File", status: "Untracked", level: 0 },
];

export function Example() {
  const [selected, setSelected] = useState("tree-grid");
  const [expanded, setExpanded] = useState(["src", "components"]);

  return (
    <div className="flex max-w-xl flex-col gap-2">
      <TreeGrid
        aria-label="Project files"
        className="w-full border-collapse overflow-hidden rounded border border-zinc-950/10 text-left text-base sm:text-sm dark:border-white/10"
        value={selected}
        onChange={setSelected}
        expanded={expanded}
        onExpandedChange={setExpanded}
      >
        <TreeGridRowGroup as="thead" className="bg-zinc-50 dark:bg-zinc-900/50">
          <TreeGridRow>
            <TreeGridColumn className="border-b border-zinc-950/10 px-2 py-1.5 font-semibold dark:border-white/10">
              Name
            </TreeGridColumn>
            <TreeGridColumn className="border-b border-zinc-950/10 px-2 py-1.5 font-semibold dark:border-white/10">
              Type
            </TreeGridColumn>
            <TreeGridColumn className="border-b border-zinc-950/10 px-2 py-1.5 font-semibold dark:border-white/10">
              Status
            </TreeGridColumn>
          </TreeGridRow>
        </TreeGridRowGroup>
        <TreeGridRowGroup>
          {projectFiles.map((file) => {
            const isFolder = file.kind === "Folder";
            const isExpanded = expanded.includes(file.value);

            return (
              <TreeGridRow
                key={file.value}
                value={file.value}
                parentValue={file.parentValue}
                className="cursor-pointer border-b border-zinc-950/5 text-zinc-800 last:border-0 data-selected:bg-teal-50 dark:border-white/5 dark:text-zinc-100 dark:data-selected:bg-teal-950/40"
              >
                <TreeGridCell
                  className="outline-teal-600 focus-visible:outline-2 dark:outline-teal-400"
                  style={{ paddingInlineStart: `${8 + file.level * 20}px` }}
                >
                  <span className="flex items-center gap-1.5 px-2 py-1.5">
                    <span className="w-3 text-zinc-400 dark:text-zinc-500" aria-hidden="true">
                      {isFolder ? (isExpanded ? "โ–พ" : "โ–ธ") : ""}
                    </span>
                    <span aria-hidden="true">{isFolder ? "๐Ÿ“" : "๐Ÿ“„"}</span>
                    {file.name}
                  </span>
                </TreeGridCell>
                <TreeGridCell className="px-2 py-1.5 text-zinc-600 outline-teal-600 focus-visible:outline-2 dark:text-zinc-400 dark:outline-teal-400">
                  {file.kind}
                </TreeGridCell>
                <TreeGridCell className="px-2 py-1.5 text-zinc-600 outline-teal-600 focus-visible:outline-2 dark:text-zinc-400 dark:outline-teal-400">
                  {file.status}
                </TreeGridCell>
              </TreeGridRow>
            );
          })}
        </TreeGridRowGroup>
      </TreeGrid>
      <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. TreeGrid

    Native table with treegrid semantics, one roving row or cell focus stop, and controlled selection and expansion. Owns a DOM element.

  2. TreeGridRowGroup

    Native table section for header or data rows; use as="thead" for column headers. Owns a DOM element.

  3. TreeGridRow

    Header row or selectable hierarchical data row. Owns a DOM element.

  4. TreeGridColumn

    Native th column header with treegrid semantics. Owns a DOM element.

  5. TreeGridCell

    Native td data cell and grid focus target. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start TreeGrid with an aria-label, then add a header TreeGridRowGroup with TreeGridColumn cells.

  2. 2

    Add the supporting parts

    Render data rows flat in a second TreeGridRowGroup; give each TreeGridRow a unique value and use parentValue to describe its parent.

  3. 3

    Make the behavior clear

    Use value/onChange for selection and defaultExpanded, or expanded with onExpandedChange, for branches. Arrow keys move through visible rows, then into their cells.

    Exampletsx
    <TreeGrid aria-label="Project files" defaultExpanded={["src"]}>
      <TreeGridRowGroup as="thead">
        <TreeGridRow>
          <TreeGridColumn>Name</TreeGridColumn>
        </TreeGridRow>
      </TreeGridRowGroup>
      <TreeGridRowGroup>
        <TreeGridRow value="src">
          <TreeGridCell>src</TreeGridCell>
        </TreeGridRow>
        <TreeGridRow value="index" parentValue="src">
          <TreeGridCell>index.ts</TreeGridCell>
        </TreeGridRow>
      </TreeGridRowGroup>
    </TreeGrid>;

Keyboard

โ‡ฅ
Moves into the tree grid at its active row or cell; Tab again leaves.
โ†“
Moves to the next visible row, preserving the focused column.
โ†‘
Moves to the previous visible row, preserving the focused column.
โ†’
Expands a collapsed row; on an expanded or leaf row, enters or moves right through cells.
โ†
Moves left through cells; from a row, collapses it or moves to its parent.
Home
Moves to the first visible row or first cell in the current row.
End
Moves to the last visible row or last cell in the current row.
CtrlHome
Moves to the first visible row while preserving the focused column.
CtrlEnd
Moves to the last visible row while preserving the focused column.
โ†ตSpace
Selects the focused row.

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 TreeGrid an aria-label that names the hierarchy, such as Project files, and use TreeGridColumn for every column header.
  • Rows announce their level, position, expansion, and selection automatically; keep the visible text in each row meaningful without its parent.
  • ArrowRight expands a row or enters its first cell, and ArrowLeft collapses it or returns to its parent. Avoid buttons and links inside cells unless their independent keyboard behavior is essential.

API reference

Importtsx
import { TreeGrid, TreeGridRow, TreeGridCell, TreeGridColumn, TreeGridRowGroup } from "@comp0/react";

TreeGrid

DOM element

Native table with treegrid semantics, one roving row or cell focus stop, and controlled selection and expansion.

PropTypeDescription
aria-labelstringNames the hierarchy; nothing labels it automatically.
valuestringControlled selected row value.
defaultValuestringInitial selected row value.
onChange(value: string) => voidReceives the next selected row value.
expandedstring[]Controlled expanded parent-row values.
defaultExpandedstring[]Initial expanded parent-row values.
onExpandedChange(expanded: string[]) => voidReceives the next expanded parent-row values.

TreeGridRowGroup

DOM element

Native table section for header or data rows; use as="thead" for column headers.

PropTypeDescription
as"thead" | "tbody"Native section to render; defaults to tbody.

TreeGridRow

DOM element

Header row or selectable hierarchical data row.

PropTypeDescription
valuestringUnique data-row identity; omit it for the column-header row.
parentValuestringParent row value; rows stay flat and in DOM order.
disabledbooleanRemoves the row from selection and keyboard navigation.

TreeGridColumn

DOM element

Native th column header with treegrid semantics.

TreeGridCell

DOM element

Native td data cell and grid focus target.

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.

TreeGridRow

Style hookMeaning
[data-selected]The row is selected.
[data-expanded]The row's branch is open.
[data-disabled]The row is disabled.
:focus-visibleThe row or cell has keyboard focus.

TreeGridCell

Style hookMeaning
:focus-visibleThe row or cell has keyboard focus.

Keep exploring