HextaUI

Tree

Nested, expandable rows for files and hierarchies, with indent guides, connector lines, cascading checkboxes, typeahead and full keyboard support.

app
layout.tsx
page.tsxM
globals.css
components
ui
button.tsxM
tree.tsxU
theme-provider.tsx
public
README.md
import {
  IconBrandTypescript,
  IconFileText,
  IconFolder,
  IconFolderOpen,
  IconPhoto,
} from "@tabler/icons-react"

import { Tree, TreeGroup, TreeItem, TreeItemLabel } from "@/components/ui/tree"

function Folder({
  value,
  name,
  children,
}: {
  value: string
  name: string
  children: React.ReactNode
}) {
  return (
    <TreeItem value={value}>
      <TreeItemLabel icon={<IconFolder />} expandedIcon={<IconFolderOpen />}>
        {name}
      </TreeItemLabel>
      <TreeGroup>{children}</TreeGroup>
    </TreeItem>
  )
}

function File({
  value,
  name,
  icon = <IconBrandTypescript />,
  status,
}: {
  value: string
  name: string
  icon?: React.ReactNode
  status?: "M" | "U"
}) {
  return (
    <TreeItem value={value}>
      <TreeItemLabel
        icon={icon}
        meta={
          status ? (
            <span
              className={status === "M" ? "text-warning" : "text-success"}
              aria-label={status === "M" ? "Modified" : "Untracked"}
            >
              {status}
            </span>
          ) : null
        }
      >
        {name}
      </TreeItemLabel>
    </TreeItem>
  )
}

export function TreeDemo() {
  return (
    <Tree
      aria-label="Project files"
      defaultExpandedValues={["app", "components", "ui"]}
      defaultSelectedValues={["button"]}
      className="w-full max-w-xs"
    >
      <Folder value="app" name="app">
        <File value="layout" name="layout.tsx" />
        <File value="page" name="page.tsx" status="M" />
        <File value="globals" name="globals.css" icon={<IconFileText />} />
      </Folder>
      <Folder value="components" name="components">
        <Folder value="ui" name="ui">
          <File value="button" name="button.tsx" status="M" />
          <File value="tree" name="tree.tsx" status="U" />
        </Folder>
        <File value="theme" name="theme-provider.tsx" />
      </Folder>
      <Folder value="public" name="public">
        <File value="og" name="og.png" icon={<IconPhoto />} />
      </Folder>
      <File value="readme" name="README.md" icon={<IconFileText />} />
    </Tree>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/tree.json

Adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

import {
  Tree,
  TreeGroup,
  TreeItem,
  TreeItemLabel,
} from "@/components/ui/tree"
<Tree aria-label="Files" defaultExpandedValues={["src"]}>
  <TreeItem value="src">
    <TreeItemLabel icon={<IconFolder />}>src</TreeItemLabel>
    <TreeGroup>
      <TreeItem value="index">
        <TreeItemLabel icon={<IconFile />}>index.ts</TreeItemLabel>
      </TreeItem>
    </TreeGroup>
  </TreeItem>
</Tree>

An item with a <TreeGroup /> is a branch: it gets a chevron, opens and closes, and answers to the arrow keys. Every item needs a value that is unique within the tree.

Tree
└── TreeItem
    ├── TreeItemLabel
    └── TreeGroup
        └── TreeItem
            └── TreeItemLabel

Indent guides

variant="lines" draws a guide for each level. The guide of the branch that holds the selected or focused row gets darker, so you can see where you are in deep trees.

packages
core
src
index.ts
store.ts
utils
clamp.ts
debounce.ts
package.json
react
turbo.json
import { IconFile, IconFolder, IconFolderOpen } from "@tabler/icons-react"

import { Tree, TreeGroup, TreeItem, TreeItemLabel } from "@/components/ui/tree"

type Node = { name: string; children?: Node[] }

const files: Node[] = [
  {
    name: "packages",
    children: [
      {
        name: "core",
        children: [
          {
            name: "src",
            children: [
              { name: "index.ts" },
              { name: "store.ts" },
              {
                name: "utils",
                children: [{ name: "clamp.ts" }, { name: "debounce.ts" }],
              },
            ],
          },
          { name: "package.json" },
        ],
      },
      {
        name: "react",
        children: [{ name: "index.tsx" }, { name: "package.json" }],
      },
    ],
  },
  { name: "turbo.json" },
]

function renderNodes(nodes: Node[], parent = ""): React.ReactNode {
  return nodes.map((node) => {
    const value = `${parent}/${node.name}`
    return node.children ? (
      <TreeItem key={value} value={value}>
        <TreeItemLabel icon={<IconFolder />} expandedIcon={<IconFolderOpen />}>
          {node.name}
        </TreeItemLabel>
        <TreeGroup>{renderNodes(node.children, value)}</TreeGroup>
      </TreeItem>
    ) : (
      <TreeItem key={value} value={value}>
        <TreeItemLabel icon={<IconFile />}>{node.name}</TreeItemLabel>
      </TreeItem>
    )
  })
}

export function TreeLines() {
  return (
    <Tree
      aria-label="Monorepo"
      variant="lines"
      defaultExpandedValues={[
        "/packages",
        "/packages/core",
        "/packages/core/src",
        "/packages/core/src/utils",
      ]}
      defaultSelectedValues={["/packages/core/src/utils/clamp.ts"]}
      className="w-full max-w-xs"
    >
      {renderNodes(files)}
    </Tree>
  )
}

Connectors

variant="connectors" joins each item to its parent with an elbow, like a terminal tree listing. Use meta for versions, sizes or counts.

next16.3.8
@next/env16.3.8
postcss8.5.6
nanoid3.3.11
picocolors1.1.1
source-map-js1.2.1
styled-jsx5.1.7
react-dom19.3.0
scheduler0.27.0
import { IconPackage } from "@tabler/icons-react"

import { Tree, TreeGroup, TreeItem, TreeItemLabel } from "@/components/ui/tree"

type Dependency = { name: string; version: string; dependencies?: Dependency[] }

const dependencies: Dependency[] = [
  {
    name: "next",
    version: "16.3.8",
    dependencies: [
      { name: "@next/env", version: "16.3.8" },
      {
        name: "postcss",
        version: "8.5.6",
        dependencies: [
          { name: "nanoid", version: "3.3.11" },
          { name: "picocolors", version: "1.1.1" },
          { name: "source-map-js", version: "1.2.1" },
        ],
      },
      { name: "styled-jsx", version: "5.1.7" },
    ],
  },
  {
    name: "react-dom",
    version: "19.3.0",
    dependencies: [{ name: "scheduler", version: "0.27.0" }],
  },
]

function renderDependencies(items: Dependency[], parent = ""): React.ReactNode {
  return items.map((item) => {
    const value = `${parent}/${item.name}`
    return (
      <TreeItem key={value} value={value}>
        <TreeItemLabel icon={<IconPackage />} meta={item.version}>
          {item.name}
        </TreeItemLabel>
        {item.dependencies ? (
          <TreeGroup>{renderDependencies(item.dependencies, value)}</TreeGroup>
        ) : null}
      </TreeItem>
    )
  })
}

export function TreeConnectors() {
  return (
    <Tree
      aria-label="Dependencies"
      variant="connectors"
      selectionMode="none"
      defaultExpandedValues={["/next", "/next/postcss", "/react-dom"]}
      className="w-full max-w-xs"
    >
      {renderDependencies(dependencies)}
    </Tree>
  )
}

Checkboxes

With checkboxes, checking a branch checks every enabled item inside it, and a partly checked branch shows a dash. Checking a branch also counts as checking children you load later. Clicking a row checks it, and the chevron opens it.

Projects
View projects
Edit projects
Delete projects
Members
Billing
View invoices
Change planOwner only

3 of 7 permissions granted

"use client"

import * as React from "react"

import { Tree, TreeGroup, TreeItem, TreeItemLabel } from "@/components/ui/tree"

const permissions = [
  {
    value: "projects",
    label: "Projects",
    children: [
      { value: "projects.view", label: "View projects" },
      { value: "projects.edit", label: "Edit projects" },
      { value: "projects.delete", label: "Delete projects" },
    ],
  },
  {
    value: "members",
    label: "Members",
    children: [
      { value: "members.invite", label: "Invite members" },
      { value: "members.remove", label: "Remove members" },
    ],
  },
  {
    value: "billing",
    label: "Billing",
    children: [
      { value: "billing.invoices", label: "View invoices" },
      { value: "billing.plan", label: "Change plan", disabled: true },
    ],
  },
]

const leaves = permissions.flatMap((group) => group.children)

export function TreeCheckboxes() {
  const [checked, setChecked] = React.useState<string[]>([
    "projects.view",
    "members.invite",
    "members.remove",
  ])
  const count = leaves.filter((leaf) => checked.includes(leaf.value)).length

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <Tree
        aria-label="Permissions"
        checkboxes
        checkedValues={checked}
        onCheckedValuesChange={setChecked}
        defaultExpandedValues={["projects", "billing"]}
      >
        {permissions.map((group) => (
          <TreeItem key={group.value} value={group.value}>
            <TreeItemLabel>{group.label}</TreeItemLabel>
            <TreeGroup>
              {group.children.map((permission) => (
                <TreeItem
                  key={permission.value}
                  value={permission.value}
                  disabled={"disabled" in permission}
                >
                  <TreeItemLabel
                    meta={"disabled" in permission ? "Owner only" : null}
                  >
                    {permission.label}
                  </TreeItemLabel>
                </TreeItem>
              ))}
            </TreeGroup>
          </TreeItem>
        ))}
      </Tree>
      <p className="text-sm text-muted-foreground" aria-live="polite">
        {count} of {leaves.length} permissions granted
      </p>
    </div>
  )
}

Render rows as links with render. Pass the current page to selectedValues, open its section, and set aria-current on its row.

import Link from "next/link"

import { Tree, TreeGroup, TreeItem, TreeItemLabel } from "@/components/ui/tree"

const sections = [
  {
    value: "getting-started",
    title: "Getting started",
    pages: [{ value: "installation", title: "Installation" }],
  },
  {
    value: "components",
    title: "Components",
    pages: [
      { value: "accordion", title: "Accordion" },
      { value: "collapsible", title: "Collapsible" },
      { value: "tabs", title: "Tabs" },
      { value: "tree", title: "Tree" },
    ],
  },
]

const current = "tree"

export function TreeNavigation() {
  return (
    <nav aria-label="Docs" className="w-full max-w-60">
      <Tree
        aria-label="Docs pages"
        size="sm"
        selectedValues={[current]}
        defaultExpandedValues={["components"]}
      >
        {sections.map((section) => (
          <TreeItem key={section.value} value={section.value}>
            <TreeItemLabel>{section.title}</TreeItemLabel>
            <TreeGroup>
              {section.pages.map((page) => (
                <TreeItem key={page.value} value={page.value}>
                  <TreeItemLabel
                    render={<Link href={`/docs/${page.value}`} />}
                    aria-current={page.value === current ? "page" : undefined}
                  >
                    {page.title}
                  </TreeItemLabel>
                </TreeItem>
              ))}
            </TreeGroup>
          </TreeItem>
        ))}
      </Tree>
    </nav>
  )
}

Multiple selection

selectionMode="multiple" works like a file manager: click to select one row, Cmd/Ctrl+click to add or remove one, Shift+click to select a range. On touch screens a tap adds or removes the row.

Photos
beach.jpg
city.jpg
forest.jpg
mountains.jpg
Documents
invoice.pdf
notes.md
resume.pdf

1 selected

"use client"

import * as React from "react"
import { IconFile, IconFolder, IconFolderOpen } from "@tabler/icons-react"

import { Tree, TreeGroup, TreeItem, TreeItemLabel } from "@/components/ui/tree"

const photos = ["beach.jpg", "city.jpg", "forest.jpg", "mountains.jpg"]
const documents = ["invoice.pdf", "notes.md", "resume.pdf"]

export function TreeMultiple() {
  const [selected, setSelected] = React.useState<string[]>(["city.jpg"])

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <Tree
        aria-label="Library"
        selectionMode="multiple"
        selectedValues={selected}
        onSelectedValuesChange={setSelected}
        defaultExpandedValues={["photos", "documents"]}
      >
        <TreeItem value="photos">
          <TreeItemLabel
            icon={<IconFolder />}
            expandedIcon={<IconFolderOpen />}
          >
            Photos
          </TreeItemLabel>
          <TreeGroup>
            {photos.map((name) => (
              <TreeItem key={name} value={name}>
                <TreeItemLabel icon={<IconFile />}>{name}</TreeItemLabel>
              </TreeItem>
            ))}
          </TreeGroup>
        </TreeItem>
        <TreeItem value="documents">
          <TreeItemLabel
            icon={<IconFolder />}
            expandedIcon={<IconFolderOpen />}
          >
            Documents
          </TreeItemLabel>
          <TreeGroup>
            {documents.map((name) => (
              <TreeItem key={name} value={name}>
                <TreeItemLabel icon={<IconFile />}>{name}</TreeItemLabel>
              </TreeItem>
            ))}
          </TreeGroup>
        </TreeItem>
      </Tree>
      <p className="text-sm text-muted-foreground" aria-live="polite">
        {selected.length === 0
          ? "Nothing selected"
          : `${selected.length} selected`}
      </p>
    </div>
  )
}

Controlled

Keep open branches in your own state with expandedValues and onExpandedValuesChange. This example also builds the tree from data with a recursive function.

src
hooks
main.tsx
tests

Selected: none

"use client"

import * as React from "react"
import { IconFile, IconFolder, IconFolderOpen } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"
import { Tree, TreeGroup, TreeItem, TreeItemLabel } from "@/components/ui/tree"

type Node = { id: string; name: string; children?: Node[] }

const nodes: Node[] = [
  {
    id: "src",
    name: "src",
    children: [
      {
        id: "hooks",
        name: "hooks",
        children: [
          { id: "use-media", name: "use-media.ts" },
          { id: "use-copy", name: "use-copy.ts" },
        ],
      },
      { id: "main", name: "main.tsx" },
    ],
  },
  {
    id: "tests",
    name: "tests",
    children: [{ id: "smoke", name: "smoke.test.ts" }],
  },
]

function branchIds(items: Node[]): string[] {
  return items.flatMap((item) =>
    item.children ? [item.id, ...branchIds(item.children)] : []
  )
}

function renderNodes(items: Node[]): React.ReactNode {
  return items.map((item) =>
    item.children ? (
      <TreeItem key={item.id} value={item.id}>
        <TreeItemLabel icon={<IconFolder />} expandedIcon={<IconFolderOpen />}>
          {item.name}
        </TreeItemLabel>
        <TreeGroup>{renderNodes(item.children)}</TreeGroup>
      </TreeItem>
    ) : (
      <TreeItem key={item.id} value={item.id}>
        <TreeItemLabel icon={<IconFile />}>{item.name}</TreeItemLabel>
      </TreeItem>
    )
  )
}

export function TreeControlled() {
  const [expanded, setExpanded] = React.useState<string[]>(["src"])
  const [selected, setSelected] = React.useState<string[]>([])

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <div className="flex gap-2">
        <Button
          variant="outline"
          size="sm"
          onClick={() => setExpanded(branchIds(nodes))}
        >
          Expand all
        </Button>
        <Button variant="outline" size="sm" onClick={() => setExpanded([])}>
          Collapse all
        </Button>
      </div>
      <Tree
        aria-label="Source"
        expandedValues={expanded}
        onExpandedValuesChange={setExpanded}
        selectedValues={selected}
        onSelectedValuesChange={setSelected}
      >
        {renderNodes(nodes)}
      </Tree>
      <p className="text-sm text-muted-foreground">
        Selected: {selected[0] ?? "none"}
      </p>
    </div>
  )
}

Lazy loading

Fetch children in onExpandedChange on <TreeItem />. Render a disabled loading row until they arrive, so the branch keeps its chevron.

api
worker
cron
"use client"

import * as React from "react"
import { IconFile, IconFolder, IconFolderOpen } from "@tabler/icons-react"

import { Spinner } from "@/components/ui/spinner"
import { Tree, TreeGroup, TreeItem, TreeItemLabel } from "@/components/ui/tree"

function listFiles(folder: string) {
  return new Promise<string[]>((resolve) =>
    setTimeout(
      () => resolve([`${folder}-1.log`, `${folder}-2.log`, `${folder}-3.log`]),
      900
    )
  )
}

function RemoteFolder({ name }: { name: string }) {
  const [files, setFiles] = React.useState<string[] | null>(null)
  const loading = React.useRef(false)

  return (
    <TreeItem
      value={name}
      onExpandedChange={(expanded) => {
        if (expanded && files === null && !loading.current) {
          loading.current = true
          listFiles(name).then(setFiles)
        }
      }}
    >
      <TreeItemLabel icon={<IconFolder />} expandedIcon={<IconFolderOpen />}>
        {name}
      </TreeItemLabel>
      <TreeGroup aria-busy={files === null}>
        {files === null ? (
          <TreeItem value={`${name}/loading`} disabled>
            <TreeItemLabel icon={<Spinner size="sm" label="Loading files" />}>
              Loading…
            </TreeItemLabel>
          </TreeItem>
        ) : (
          files.map((file) => (
            <TreeItem key={file} value={`${name}/${file}`}>
              <TreeItemLabel icon={<IconFile />}>{file}</TreeItemLabel>
            </TreeItem>
          ))
        )}
      </TreeGroup>
    </TreeItem>
  )
}

export function TreeLazy() {
  return (
    <Tree aria-label="Server logs" className="w-full max-w-xs">
      <RemoteFolder name="api" />
      <RemoteFolder name="worker" />
      <RemoteFolder name="cron" />
    </Tree>
  )
}

Small

size="sm" uses shorter rows and tighter indents, for sidebars. Rows grow on touch screens in both sizes.

Text channels
general12
design
releases3
Voice channels
Lounge
import { IconHash, IconVolume } from "@tabler/icons-react"

import { Tree, TreeGroup, TreeItem, TreeItemLabel } from "@/components/ui/tree"

export function TreeSmall() {
  return (
    <Tree
      aria-label="Channels"
      size="sm"
      defaultExpandedValues={["text", "voice"]}
      defaultSelectedValues={["general"]}
      className="w-full max-w-56"
    >
      <TreeItem value="text">
        <TreeItemLabel>Text channels</TreeItemLabel>
        <TreeGroup>
          <TreeItem value="general">
            <TreeItemLabel icon={<IconHash />} meta="12">
              general
            </TreeItemLabel>
          </TreeItem>
          <TreeItem value="design">
            <TreeItemLabel icon={<IconHash />}>design</TreeItemLabel>
          </TreeItem>
          <TreeItem value="releases">
            <TreeItemLabel icon={<IconHash />} meta="3">
              releases
            </TreeItemLabel>
          </TreeItem>
        </TreeGroup>
      </TreeItem>
      <TreeItem value="voice">
        <TreeItemLabel>Voice channels</TreeItemLabel>
        <TreeGroup>
          <TreeItem value="lounge">
            <TreeItemLabel icon={<IconVolume />}>Lounge</TreeItemLabel>
          </TreeItem>
        </TreeGroup>
      </TreeItem>
    </Tree>
  )
}

Disabled

A disabled item can’t be selected, checked or opened, and the arrow keys skip it. Items inside a disabled branch are disabled too.

Team
roadmap.md
salaries.xlsx
notes.md
Archive
import { IconFile, IconFolder, IconLock } from "@tabler/icons-react"

import { Tree, TreeGroup, TreeItem, TreeItemLabel } from "@/components/ui/tree"

export function TreeDisabled() {
  return (
    <Tree
      aria-label="Shared drive"
      defaultExpandedValues={["team"]}
      className="w-full max-w-xs"
    >
      <TreeItem value="team">
        <TreeItemLabel icon={<IconFolder />}>Team</TreeItemLabel>
        <TreeGroup>
          <TreeItem value="roadmap">
            <TreeItemLabel icon={<IconFile />}>roadmap.md</TreeItemLabel>
          </TreeItem>
          <TreeItem value="salaries" disabled>
            <TreeItemLabel icon={<IconLock />}>salaries.xlsx</TreeItemLabel>
          </TreeItem>
          <TreeItem value="notes">
            <TreeItemLabel icon={<IconFile />}>notes.md</TreeItemLabel>
          </TreeItem>
        </TreeGroup>
      </TreeItem>
      <TreeItem value="archive" disabled>
        <TreeItemLabel icon={<IconLock />}>Archive</TreeItemLabel>
        <TreeGroup>
          <TreeItem value="2024">
            <TreeItemLabel icon={<IconFile />}>2024.zip</TreeItemLabel>
          </TreeItem>
        </TreeGroup>
      </TreeItem>
    </Tree>
  )
}

Long names

Names cut off with an ellipsis and meta stays visible. Pass title to show the full name on hover.

quarterly-financial-reports-and-board-presentations
2026-q3-consolidated-revenue-forecast-final-v12-approved.xlsx2.4 MB
averyveryverylongfilenamewithoutanyspacesoranybreaks.txt
import { IconFile, IconFolder, IconFolderOpen } from "@tabler/icons-react"

import { Tree, TreeGroup, TreeItem, TreeItemLabel } from "@/components/ui/tree"

const longFolder = "quarterly-financial-reports-and-board-presentations"
const longFile = "2026-q3-consolidated-revenue-forecast-final-v12-approved.xlsx"
const unbroken = "averyveryverylongfilenamewithoutanyspacesoranybreaks.txt"

export function TreeLongNames() {
  return (
    <Tree
      aria-label="Reports"
      variant="lines"
      defaultExpandedValues={["reports"]}
      className="w-full max-w-56"
    >
      <TreeItem value="reports">
        <TreeItemLabel
          icon={<IconFolder />}
          expandedIcon={<IconFolderOpen />}
          title={longFolder}
        >
          {longFolder}
        </TreeItemLabel>
        <TreeGroup>
          <TreeItem value="forecast">
            <TreeItemLabel icon={<IconFile />} meta="2.4 MB" title={longFile}>
              {longFile}
            </TreeItemLabel>
          </TreeItem>
          <TreeItem value="unbroken">
            <TreeItemLabel icon={<IconFile />} title={unbroken}>
              {unbroken}
            </TreeItemLabel>
          </TreeItem>
        </TreeGroup>
      </TreeItem>
    </Tree>
  )
}

Right to left

Indents, connectors and chevrons mirror, and the left and right arrow keys swap.

المستندات
الفواتير
مارس.pdf١٢٠ ك.ب
أبريل.pdf
العقد.docx
ملاحظات.txt
import { IconFile, IconFolder, IconFolderOpen } from "@tabler/icons-react"

import { Tree, TreeGroup, TreeItem, TreeItemLabel } from "@/components/ui/tree"

export function TreeRtl() {
  return (
    <div dir="rtl" className="w-full max-w-xs">
      <Tree
        aria-label="المستندات"
        variant="connectors"
        defaultExpandedValues={["docs", "invoices"]}
        defaultSelectedValues={["march"]}
      >
        <TreeItem value="docs">
          <TreeItemLabel
            icon={<IconFolder />}
            expandedIcon={<IconFolderOpen />}
          >
            المستندات
          </TreeItemLabel>
          <TreeGroup>
            <TreeItem value="invoices">
              <TreeItemLabel
                icon={<IconFolder />}
                expandedIcon={<IconFolderOpen />}
              >
                الفواتير
              </TreeItemLabel>
              <TreeGroup>
                <TreeItem value="march">
                  <TreeItemLabel icon={<IconFile />} meta="١٢٠ ك.ب">
                    مارس.pdf
                  </TreeItemLabel>
                </TreeItem>
                <TreeItem value="april">
                  <TreeItemLabel icon={<IconFile />}>أبريل.pdf</TreeItemLabel>
                </TreeItem>
              </TreeGroup>
            </TreeItem>
            <TreeItem value="contract">
              <TreeItemLabel icon={<IconFile />}>العقد.docx</TreeItemLabel>
            </TreeItem>
          </TreeGroup>
        </TreeItem>
        <TreeItem value="notes">
          <TreeItemLabel icon={<IconFile />}>ملاحظات.txt</TreeItemLabel>
        </TreeItem>
      </Tree>
    </div>
  )
}
KeyAction
↓Moves focus to the next visible row.
↑Moves focus to the previous visible row.
→Opens a closed branch. On an open branch, moves focus to its first child.
←Closes an open branch. Otherwise moves focus to the parent.
HomeMoves focus to the first row.
EndMoves focus to the last visible row.
EnterSelects the row and opens or closes it if it is a branch. Follows the link on link rows.
SpaceSelects the row, adds or removes it in multiple mode, or checks it with checkboxes.
Shift + ↑Shift + ↓Extends the selection in multiple mode.
Cmd/Ctrl + ASelects every visible row in multiple mode.
*Opens every branch at the focused row’s level.
a–zMoves focus to the next row whose name starts with the typed letters.

The tree follows the WAI-ARIA tree view pattern. Each row is the treeitem and owns its group through aria-owns, so a row’s name is only its own label and link rows work as real links. Only one row is in the tab order at a time. When a branch closes around the focused row, focus moves to that branch instead of getting lost.

Closed groups stay in the page, so the browser’s find-in-page opens the folders around a match, and checking a branch sees every item inside it. Give the tree an aria-label.

Branches open and close with the Base UI collapsible. <TreeItem /> and <TreeGroup /> accept the props of the parts they wrap.

PropTypeDefault
variant
"default" | "lines" | "connectors""default"
size
"default" | "sm""default"
selectionMode
"none" | "single" | "multiple""single"
selectedValues
string[]–
defaultSelectedValues
string[]–
onSelectedValuesChange
(values: string[]) => void–
expandedValues
string[]–
defaultExpandedValues
string[]–
onExpandedValuesChange
(values: string[]) => void–
checkboxesShow cascading checkboxes. Rows are checked instead of selected.
booleanfalse
checkedValuesChecked items. A branch in the list counts as checking its children.
string[]–
defaultCheckedValues
string[]–
onCheckedValuesChangeCalled with every checked leaf and every fully checked branch.
(values: string[]) => void–
disabled
booleanfalse
hiddenUntilFoundLet find-in-page open closed branches around a match.
booleantrue
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="tree"Target the root in CSS.
data-variantThe current variant.
data-sizeThe current size.
--tree-row-heightRow height. Larger on touch screens.
--tree-indentHow far each level is indented.
PropTypeDefault
valueRequired. Unique within the tree.
string–
disabled
booleanfalse
onExpandedChangeCalled when this item opens or closes.
(expanded: boolean) => void–
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="tree-item"Target items in CSS.
data-openPresent when the item is open.
data-disabledPresent when the item is disabled.
PropTypeDefault
iconShown before the label.
ReactNode–
expandedIconCrossfades in for icon while the branch is open.
ReactNode–
metaShown at the end of the row, such as a size, count or status.
ReactNode–
className
string–
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="tree-item-label"Target rows in CSS.
data-valueThe item’s value.
data-expandedPresent when the branch is open.
data-selectedPresent when the row is selected.
data-checkedPresent when the row is checked.
data-indeterminatePresent when only some children are checked.
data-disabledPresent when the item is disabled.
PropTypeDefault
classNameApplied to the inner wrapper, so padding never fights the height animation.
string–
AttributeDescription
data-slot="tree-group"Target groups in CSS.
data-openPresent when the group is open.
data-starting-stylePresent while the group animates in.
data-ending-stylePresent while the group animates out.
--collapsible-panel-heightThe group’s measured height, used for the height animation.