# Tree

> A tree view on Vercel's UI SDK for files and nested pages, with file paths, checkboxes, links and children that load on demand.

Canonical: https://hextaui.com/docs/ui-sdk-tree
Last updated: 2026-11-13

> **Beta.** Built on Vercel’s UI SDK 0.1.0-beta.1 (`@ui-sdk/react`), a public beta. Its API may change.

```tsx title="components/examples/ui-sdk-tree/demo.tsx"
import {
  IconBrandTypescript,
  IconFileText,
  IconFolder,
  IconFolderOpen,
} from "@tabler/icons-react"

import {
  Tree,
  TreeGroup,
  TreeItem,
  TreeItemIndicator,
  TreeItemLabel,
  type TreeItemData,
} from "@/components/ui/ui-sdk-tree"

const files: TreeItemData[] = [
  {
    value: "app",
    label: "app",
    children: [
      { value: "app/layout.tsx", label: "layout.tsx" },
      { value: "app/page.tsx", label: "page.tsx" },
      { value: "app/globals.css", label: "globals.css" },
    ],
  },
  {
    value: "components",
    label: "components",
    children: [
      {
        value: "components/ui",
        label: "ui",
        children: [
          { value: "components/ui/button.tsx", label: "button.tsx" },
          { value: "components/ui/tree.tsx", label: "tree.tsx" },
        ],
      },
      { value: "components/theme.tsx", label: "theme.tsx" },
    ],
  },
  { value: "package.json", label: "package.json" },
  { value: "README.md", label: "README.md" },
]

function Items({ items }: { items: TreeItemData[] }) {
  return items.map((item) => (
    <TreeItem key={item.value} value={item.value}>
      <TreeItemLabel>
        {item.children ? (
          <>
            <TreeItemIndicator />
            <IconFolder className="group-data-expanded/tree-label:hidden" />
            <IconFolderOpen className="not-group-data-expanded/tree-label:hidden" />
          </>
        ) : item.label.endsWith(".tsx") ? (
          <IconBrandTypescript />
        ) : (
          <IconFileText />
        )}
        <span>{item.label}</span>
      </TreeItemLabel>
      {item.children && (
        <TreeGroup>
          <Items items={item.children} />
        </TreeGroup>
      )}
    </TreeItem>
  ))
}

function UiSdkTreeDemo() {
  return (
    <Tree
      aria-label="Project files"
      items={files}
      defaultExpanded={["components", "components/ui"]}
      defaultValue="components/ui/tree.tsx"
      className="max-w-xs"
    >
      <Items items={files} />
    </Tree>
  )
}

export { UiSdkTreeDemo }
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/ui-sdk-tree.json
```

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

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @ui-sdk/react@0.1.0-beta.1 @ui-sdk/core@0.1.0-beta.1 @tabler/icons-react class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/ui-sdk-tree.tsx"
"use client"

import * as React from "react"
import {
  IconCheck,
  IconChevronRight,
  IconLoader2,
  IconMinus,
  IconRefresh,
} from "@tabler/icons-react"
import * as TreePrimitive from "@ui-sdk/react/tree"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

const treeVariants = cva(
  "group/tree relative flex w-full min-w-0 flex-col text-sm text-foreground [--tree-chevron:--spacing(4)] [--tree-indent:calc(var(--tree-chevron)+var(--tree-gap))] [--tree-leaf:0px] has-data-[slot=tree-indicator]:[--tree-leaf:var(--tree-indent)]",
  {
    variants: {
      variant: {
        default: "",
        lines: "",
      },
      size: {
        default:
          "[--tree-gap:--spacing(1.5)] [--tree-row-height:--spacing(8)] [--tree-row-px:--spacing(2)] pointer-coarse:[--tree-row-height:--spacing(10)]",
        sm: "[--tree-gap:--spacing(1)] [--tree-row-height:--spacing(7)] [--tree-row-px:--spacing(1.5)] pointer-coarse:[--tree-row-height:--spacing(9)]",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

type TreeProps<Mode extends TreePrimitive.TreeMode = "single"> =
  TreePrimitive.TreeProps<Mode> & VariantProps<typeof treeVariants>

function Tree<Mode extends TreePrimitive.TreeMode = "single">({
  className,
  variant = "default",
  size = "default",
  children,
  ...props
}: TreeProps<Mode>) {
  const { items, paths } = props
  const data = React.useMemo(
    () => items ?? (paths ? TreePrimitive.treeFromPaths(paths) : undefined),
    [items, paths]
  )

  return (
    <TreePrimitive.Tree<Mode>
      data-variant={variant}
      data-size={size}
      className={mergeClassName(treeVariants({ variant, size }), className)}
      {...props}
    >
      {children ?? (data ? <TreeItems items={data} /> : null)}
    </TreePrimitive.Tree>
  )
}

function TreeItem({
  className,
  ...props
}: React.ComponentProps<typeof TreePrimitive.TreeItem>) {
  return (
    <TreePrimitive.TreeItem
      className={mergeClassName(
        "relative flex min-w-0 flex-col outline-none [&:focus-visible>[data-slot=tree-label]]:z-10 [&:focus-visible>[data-slot=tree-label]]:ring-3 [&:focus-visible>[data-slot=tree-label]]:ring-focus-ring [&:focus-visible>[data-slot=tree-label]]:outline-1 [&:focus-visible>[data-slot=tree-label]]:-outline-offset-1 [&:focus-visible>[data-slot=tree-label]]:outline-ring [&:focus-visible>[data-slot=tree-label]]:outline-solid [&:focus-visible>[data-slot=tree-label]]:ring-inset",
        className
      )}
      {...props}
    />
  )
}

function TreeItemLabel({
  className,
  ...props
}: React.ComponentProps<typeof TreePrimitive.TreeItemLabel>) {
  return (
    <TreePrimitive.TreeItemLabel
      className={mergeClassName(
        "group/tree-label relative flex h-(--tree-row-height) min-w-0 shrink-0 cursor-default items-center gap-(--tree-gap) rounded-md px-(--tree-row-px) text-start text-foreground no-underline transition-[background-color,color] duration-150 select-none [-webkit-tap-highlight-color:transparent] not-data-branch:ps-[calc(var(--tree-row-px)+var(--tree-leaf))] hover:bg-muted/60 data-current:bg-muted data-current:font-medium data-pending:text-muted-foreground data-[selected]:bg-muted data-[selected]:hover:bg-muted forced-colors:data-current:outline-2 forced-colors:data-current:-outline-offset-2 forced-colors:data-current:outline-solid forced-colors:data-[selected]:outline-2 forced-colors:data-[selected]:-outline-offset-2 forced-colors:data-[selected]:outline-solid data-disabled:cursor-not-allowed data-disabled:opacity-50 data-disabled:hover:bg-transparent [&_svg]:pointer-events-none [&_svg]:shrink-0 [&>span:not([data-slot])]:truncate [&>svg]:text-muted-foreground [&>svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    />
  )
}

function TreeItemIndicator({
  className,
  children,
  ...props
}: React.ComponentProps<typeof TreePrimitive.TreeItemIndicator>) {
  return (
    <TreePrimitive.TreeItemIndicator
      className={mergeClassName(
        "relative grid size-(--tree-chevron) shrink-0 place-items-center text-muted-foreground transition-colors duration-150 group-hover/tree-label:text-foreground after:absolute after:-inset-x-1 after:-inset-y-2 rtl:-scale-x-100 [&>svg]:size-4 [&>svg]:transition-[rotate] [&>svg]:duration-200 [&>svg]:ease-out-quint data-expanded:[&>svg]:rotate-90 motion-reduce:[&>svg]:transition-none",
        className
      )}
      {...props}
    >
      {children ??
        ((state) =>
          state.loading ? (
            <IconLoader2
              aria-hidden="true"
              className="motion-safe:animate-spin"
            />
          ) : state.failed ? (
            <IconRefresh aria-hidden="true" className="text-destructive" />
          ) : (
            <IconChevronRight aria-hidden="true" />
          ))}
    </TreePrimitive.TreeItemIndicator>
  )
}

function TreeItemCheckbox({
  className,
  children,
  ...props
}: React.ComponentProps<typeof TreePrimitive.TreeItemCheckbox>) {
  return (
    <TreePrimitive.TreeItemCheckbox
      className={mergeClassName(
        "relative inline-flex size-4 shrink-0 items-center justify-center rounded-[calc(var(--radius-sm)*0.5)] bg-background text-primary-foreground inset-ring-(length:--hairline) inset-ring-muted-foreground/80 transition-[background-color,box-shadow] duration-150 ease-out-quint group-hover/tree-label:inset-ring-foreground/70 data-indeterminate:bg-primary data-indeterminate:inset-ring-primary dark:bg-input/30 dark:data-indeterminate:bg-primary forced-colors:border data-checked:bg-primary data-checked:inset-ring-primary dark:data-checked:bg-primary [&>svg]:size-3.5",
        className
      )}
      {...props}
    >
      {children ??
        ((state) =>
          state.checked === "indeterminate" ? (
            <IconMinus aria-hidden="true" stroke={2.75} />
          ) : state.checked ? (
            <IconCheck aria-hidden="true" stroke={2.75} />
          ) : null)}
    </TreePrimitive.TreeItemCheckbox>
  )
}

function TreeGroup({
  className,
  ...props
}: React.ComponentProps<typeof TreePrimitive.TreeGroup>) {
  return (
    <TreePrimitive.TreeGroup
      className={mergeClassName(
        "relative flex h-(--tree-group-height) min-w-0 flex-col overflow-hidden ps-(--tree-indent) transition-[height] duration-200 ease-out-quint group-data-[variant=lines]/tree:before:pointer-events-none group-data-[variant=lines]/tree:before:absolute group-data-[variant=lines]/tree:before:inset-y-0 group-data-[variant=lines]/tree:before:start-[calc(var(--tree-row-px)+var(--tree-chevron)/2)] group-data-[variant=lines]/tree:before:border-s group-data-[variant=lines]/tree:before:border-border data-ending-style:h-0 data-starting-style:h-0 motion-reduce:transition-none",
        className
      )}
      {...props}
    />
  )
}

function TreeItems({ items }: { items: TreePrimitive.TreeItemData[] }) {
  return items.map((item) => (
    <TreeItem
      key={item.value}
      value={item.value}
      disabled={item.disabled}
      branch={item.children ? true : undefined}
    >
      <TreeItemLabel render={item.href ? <a href={item.href} /> : undefined}>
        {item.children && <TreeItemIndicator />}
        <span>{item.label}</span>
      </TreeItemLabel>
      {item.children && item.children.length > 0 && (
        <TreeGroup>
          <TreeItems items={item.children} />
        </TreeGroup>
      )}
    </TreeItem>
  ))
}

const createTreeController = TreePrimitive.createTreeController
const treeFromPaths = TreePrimitive.treeFromPaths
const visibleTreeRows = TreePrimitive.visibleTreeRows

export {
  Tree,
  TreeItem,
  TreeItemLabel,
  TreeItemIndicator,
  TreeItemCheckbox,
  TreeGroup,
  TreeItems,
  createTreeController,
  treeFromPaths,
  treeVariants,
  visibleTreeRows,
}
export type { TreeProps }
export type {
  TreeItemData,
  TreeMode,
  TreeValue,
  TreeCheckStrategy,
  TreeController,
} from "@ui-sdk/react/tree"
```

Update the import paths to match your project setup.

## Usage

UI SDK adds a little CSS of its own in a cascade layer named `ui-sdk`. Declare that layer at the top of your global stylesheet, above the Tailwind import, so your classes win over it.

```css
@layer ui-sdk;

@import "tailwindcss";
```

```tsx
import {
  Tree,
  TreeGroup,
  TreeItem,
  TreeItemIndicator,
  TreeItemLabel,
} from "@/components/ui/ui-sdk-tree"
```

```tsx
<Tree aria-label="Project files">
  <TreeItem value="src">
    <TreeItemLabel>
      <TreeItemIndicator />
      <span>src</span>
    </TreeItemLabel>
    <TreeGroup>
      <TreeItem value="src/index.ts">
        <TreeItemLabel>
          <span>index.ts</span>
        </TreeItemLabel>
      </TreeItem>
    </TreeGroup>
  </TreeItem>
</Tree>
```

An item with a `TreeGroup` inside it is a branch. Give every item a unique `value` and the tree an accessible name. Focus stays on the item, and its label is the row you see, so selected, current and focused styles all land on the label.

## Composition

```text
Tree
└── TreeItem
    ├── TreeItemLabel
    │   ├── TreeItemIndicator
    │   └── TreeItemCheckbox
    └── TreeGroup
        └── TreeItem
```

## Examples

### Items as data

Pass `items` and no children, and the tree renders styled rows with chevrons for you. Each item has a `value`, a `label` and optional `children`.

```tsx title="components/examples/ui-sdk-tree/data.tsx"
import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "fruit",
    label: "Fruit",
    children: [
      { value: "apple", label: "Apple" },
      { value: "banana", label: "Banana" },
      { value: "cherry", label: "Cherry" },
    ],
  },
  {
    value: "vegetables",
    label: "Vegetables",
    children: [
      { value: "carrot", label: "Carrot" },
      { value: "leek", label: "Leek" },
    ],
  },
  { value: "bread", label: "Bread" },
]

function UiSdkTreeData() {
  return (
    <Tree
      aria-label="Groceries"
      items={items}
      defaultExpanded={["fruit"]}
      className="max-w-xs"
    />
  )
}

export { UiSdkTreeData }
```

### File paths

Pass `paths` for a file explorer, with folders first. With `activation="automatic"` the arrow keys select, and an `onAction` that returns a promise marks the row pending until it settles.

```tsx title="components/examples/ui-sdk-tree/paths.tsx"
"use client"

import * as React from "react"
import { IconLoader2 } from "@tabler/icons-react"

import {
  Tree,
  TreeGroup,
  TreeItem,
  TreeItemIndicator,
  TreeItemLabel,
  treeFromPaths,
  type TreeItemData,
} from "@/components/ui/ui-sdk-tree"

const paths = [
  "src/index.ts",
  "src/lib/format.ts",
  "src/lib/parse.ts",
  "src/routes/home.tsx",
  "src/routes/about.tsx",
  "tests/",
  "package.json",
  "tsconfig.json",
]

const items = treeFromPaths(paths)
const files = new Set(paths.filter((path) => !path.endsWith("/")))

function Items({ items }: { items: TreeItemData[] }) {
  return items.map((item) => (
    <TreeItem key={item.value} value={item.value}>
      <TreeItemLabel>
        {(state) => (
          <>
            {item.children && <TreeItemIndicator />}
            <span>{item.label}</span>
            {state.pending && (
              <IconLoader2 className="ms-auto motion-safe:animate-spin" />
            )}
          </>
        )}
      </TreeItemLabel>
      {item.children && item.children.length > 0 && (
        <TreeGroup>
          <Items items={item.children} />
        </TreeGroup>
      )}
    </TreeItem>
  ))
}

function UiSdkTreePaths() {
  const [opened, setOpened] = React.useState<string | null>(null)

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <Tree
        aria-label="Project"
        paths={paths}
        defaultExpanded={["src"]}
        defaultValue="src/index.ts"
        activation="automatic"
        onAction={async (value) => {
          if (!files.has(value)) {
            return
          }
          await new Promise((resolve) => setTimeout(resolve, 600))
          setOpened(value)
        }}
      >
        <Items items={items} />
      </Tree>
      <p className="text-sm text-muted-foreground" aria-live="polite">
        {opened ? `Opened ${opened}` : "Press Enter to open a file."}
      </p>
    </div>
  )
}

export { UiSdkTreePaths }
```

### Multiple selection

With `mode="multiple"`, Cmd or Ctrl-click toggles an item and Shift extends a range. `expandOnPress={false}` leaves expanding to the chevron and the arrow keys.

```tsx title="components/examples/ui-sdk-tree/multiple.tsx"
"use client"

import * as React from "react"

import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "inbox",
    label: "Inbox",
    children: [
      { value: "updates", label: "Updates" },
      { value: "forums", label: "Forums" },
      { value: "promotions", label: "Promotions" },
    ],
  },
  {
    value: "projects",
    label: "Projects",
    children: [
      { value: "website", label: "Website" },
      { value: "mobile", label: "Mobile app" },
    ],
  },
  { value: "archive", label: "Archive" },
  { value: "trash", label: "Trash" },
]

function UiSdkTreeMultiple() {
  const [value, setValue] = React.useState(["updates", "website"])

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <Tree
        aria-label="Folders"
        items={items}
        mode="multiple"
        value={value}
        onValueChange={setValue}
        expandOnPress={false}
        defaultExpanded={["inbox", "projects"]}
      />
      <p className="text-sm text-muted-foreground">{value.length} selected</p>
    </div>
  )
}

export { UiSdkTreeMultiple }
```

### Checkboxes

Add `checkable` and a `TreeItemCheckbox` in each label. Checking a branch checks everything in it, and a partly checked branch shows a dash.

```tsx title="components/examples/ui-sdk-tree/checkboxes.tsx"
"use client"

import * as React from "react"

import {
  Tree,
  TreeGroup,
  TreeItem,
  TreeItemCheckbox,
  TreeItemIndicator,
  TreeItemLabel,
  type TreeItemData,
} from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "frontend",
    label: "Frontend",
    children: [
      { value: "react", label: "React" },
      { value: "vue", label: "Vue" },
      { value: "svelte", label: "Svelte" },
    ],
  },
  {
    value: "backend",
    label: "Backend",
    children: [
      { value: "node", label: "Node" },
      {
        value: "databases",
        label: "Databases",
        children: [
          { value: "postgres", label: "Postgres" },
          { value: "sqlite", label: "SQLite" },
        ],
      },
    ],
  },
]

function Items({ items }: { items: TreeItemData[] }) {
  return items.map((item) => (
    <TreeItem key={item.value} value={item.value}>
      <TreeItemLabel>
        {item.children && <TreeItemIndicator />}
        <TreeItemCheckbox />
        <span>{item.label}</span>
      </TreeItemLabel>
      {item.children && (
        <TreeGroup>
          <Items items={item.children} />
        </TreeGroup>
      )}
    </TreeItem>
  ))
}

function UiSdkTreeCheckboxes() {
  const [checked, setChecked] = React.useState(["react", "postgres"])

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <Tree
        aria-label="Stack"
        items={items}
        mode="none"
        checkable
        checked={checked}
        onCheckedChange={setChecked}
        defaultExpanded={["frontend", "backend", "databases"]}
      >
        <Items items={items} />
      </Tree>
      <p className="text-sm text-muted-foreground">
        {checked.length > 0 ? checked.join(", ") : "Nothing checked"}
      </p>
    </div>
  )
}

export { UiSdkTreeCheckboxes }
```

### Navigation

Items with an `href` render as links. With `mode="none"` and `current`, the current page gets `aria-current`, its parents open and it takes the tab stop.

```tsx title="components/examples/ui-sdk-tree/links.tsx"
"use client"

import * as React from "react"

import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const pages: TreeItemData[] = [
  {
    value: "getting-started",
    label: "Getting started",
    children: [
      { value: "introduction", label: "Introduction", href: "#introduction" },
      { value: "installation", label: "Installation", href: "#installation" },
    ],
  },
  {
    value: "components",
    label: "Components",
    children: [
      { value: "accordion", label: "Accordion", href: "#accordion" },
      { value: "dialog", label: "Dialog", href: "#dialog" },
      { value: "tree", label: "Tree", href: "#tree" },
    ],
  },
  { value: "changelog", label: "Changelog", href: "#changelog" },
]

function UiSdkTreeLinks() {
  const [current, setCurrent] = React.useState("tree")

  return (
    <Tree
      aria-label="Docs"
      items={pages}
      mode="none"
      current={current}
      onAction={(value, { event }) => {
        if (value === "getting-started" || value === "components") {
          return
        }
        event?.preventDefault()
        setCurrent(value)
      }}
      className="max-w-xs"
    />
  )
}

export { UiSdkTreeLinks }
```

### Loading children

Mark an item `branch` before its children exist, and `loadChildren` runs the first time it opens. The chevron turns into a spinner while it loads, and into a retry icon if it fails. Archive fails once.

```tsx title="components/examples/ui-sdk-tree/lazy.tsx"
"use client"

import * as React from "react"

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

type Row = { value: string; label: string; branch?: boolean }

const top: Row[] = [
  { value: "users", label: "users", branch: true },
  { value: "orders", label: "orders", branch: true },
  { value: "archive", label: "archive", branch: true },
]

async function fetchRows(value: string, signal: AbortSignal, fail: boolean) {
  await new Promise((resolve, reject) => {
    const timer = setTimeout(resolve, 800)
    signal.addEventListener("abort", () => {
      clearTimeout(timer)
      reject(signal.reason)
    })
  })
  if (fail) {
    throw new Error("Network error")
  }
  return Array.from({ length: 3 }, (_, index) => ({
    value: `${value}/${index + 1}`,
    label: `${value}_${index + 1}.json`,
  }))
}

function Items({
  items,
  loaded,
}: {
  items: Row[]
  loaded: Record<string, Row[]>
}) {
  return items.map((item) => (
    <TreeItem key={item.value} value={item.value} branch={item.branch}>
      <TreeItemLabel>
        {item.branch && <TreeItemIndicator />}
        <span>{item.label}</span>
      </TreeItemLabel>
      {loaded[item.value] && (
        <TreeGroup>
          <Items items={loaded[item.value]} loaded={loaded} />
        </TreeGroup>
      )}
    </TreeItem>
  ))
}

function UiSdkTreeLazy() {
  const [loaded, setLoaded] = React.useState<Record<string, Row[]>>({})
  const failed = React.useRef(false)

  return (
    <Tree
      aria-label="Database"
      loadChildren={async (value, { signal }) => {
        const fail = value === "archive" && !failed.current
        failed.current ||= fail
        const rows = await fetchRows(value, signal, fail)
        setLoaded((current) => ({ ...current, [value]: rows }))
      }}
      className="max-w-xs"
    >
      <Items items={top} loaded={loaded} />
    </Tree>
  )
}

export { UiSdkTreeLazy }
```

### Lines and small size

`variant="lines"` draws an indent guide for each open group, and `size="sm"` makes the rows shorter.

```tsx title="components/examples/ui-sdk-tree/lines.tsx"
import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "src",
    label: "src",
    children: [
      {
        value: "src/components",
        label: "components",
        children: [
          { value: "src/components/header.tsx", label: "header.tsx" },
          { value: "src/components/footer.tsx", label: "footer.tsx" },
        ],
      },
      { value: "src/main.tsx", label: "main.tsx" },
    ],
  },
  { value: "vite.config.ts", label: "vite.config.ts" },
]

function UiSdkTreeLines() {
  return (
    <Tree
      aria-label="Source"
      items={items}
      variant="lines"
      size="sm"
      defaultExpanded={["src", "src/components"]}
      className="max-w-xs"
    />
  )
}

export { UiSdkTreeLines }
```

### Controller

A controller from `createTreeController()` expands, collapses, selects and reveals items from anywhere.

```tsx title="components/examples/ui-sdk-tree/controller.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  createTreeController,
  Tree,
  type TreeItemData,
} from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "design",
    label: "Design",
    children: [
      { value: "tokens", label: "Tokens" },
      {
        value: "icons",
        label: "Icons",
        children: [
          { value: "outline", label: "Outline" },
          { value: "filled", label: "Filled" },
        ],
      },
    ],
  },
  {
    value: "engineering",
    label: "Engineering",
    children: [
      { value: "api", label: "API" },
      { value: "web", label: "Web" },
    ],
  },
]

function UiSdkTreeController() {
  const [tree] = React.useState(createTreeController)

  return (
    <div className="flex w-full max-w-xs flex-col gap-3">
      <div className="flex gap-2">
        <Button variant="outline" size="sm" onClick={() => tree.expandAll()}>
          Expand all
        </Button>
        <Button variant="outline" size="sm" onClick={() => tree.collapseAll()}>
          Collapse all
        </Button>
        <Button variant="ghost" size="sm" onClick={() => tree.reveal("filled")}>
          Reveal Filled
        </Button>
      </div>
      <Tree aria-label="Teams" items={items} controller={tree} />
    </div>
  )
}

export { UiSdkTreeController }
```

### Find in page

With `hiddenUntilFound`, closed groups stay in the page, so the browser’s Find in Page can match Phobos and open Mars to show it.

```tsx title="components/examples/ui-sdk-tree/find-in-page.tsx"
import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "planets",
    label: "Planets",
    children: [
      { value: "mercury", label: "Mercury" },
      { value: "venus", label: "Venus" },
      {
        value: "earth",
        label: "Earth",
        children: [{ value: "moon", label: "The Moon" }],
      },
      {
        value: "mars",
        label: "Mars",
        children: [
          { value: "phobos", label: "Phobos" },
          { value: "deimos", label: "Deimos" },
        ],
      },
    ],
  },
  {
    value: "dwarf-planets",
    label: "Dwarf planets",
    children: [
      { value: "ceres", label: "Ceres" },
      {
        value: "pluto",
        label: "Pluto",
        children: [{ value: "charon", label: "Charon" }],
      },
    ],
  },
]

function UiSdkTreeFindInPage() {
  return (
    <Tree
      aria-label="Solar system"
      items={items}
      hiddenUntilFound
      className="max-w-xs"
    />
  )
}

export { UiSdkTreeFindInPage }
```

### Disabled

A disabled item can still take focus, so screen readers find it, but it can’t be selected, checked or opened.

```tsx title="components/examples/ui-sdk-tree/disabled.tsx"
import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "workspace",
    label: "Workspace",
    children: [
      { value: "general", label: "General" },
      { value: "billing", label: "Billing", disabled: true },
      { value: "members", label: "Members" },
    ],
  },
  { value: "danger", label: "Danger zone", disabled: true },
]

function UiSdkTreeDisabled() {
  return (
    <Tree
      aria-label="Settings"
      items={items}
      defaultExpanded={["workspace"]}
      defaultValue="general"
      className="max-w-xs"
    />
  )
}

export { UiSdkTreeDisabled }
```

### Right to left

Rows indent from the right, the chevron points left, and the Left and Right arrow keys swap.

```tsx title="components/examples/ui-sdk-tree/rtl.tsx"
import { Tree, type TreeItemData } from "@/components/ui/ui-sdk-tree"

const items: TreeItemData[] = [
  {
    value: "documents",
    label: "المستندات",
    children: [
      { value: "reports", label: "التقارير" },
      { value: "invoices", label: "الفواتير" },
    ],
  },
  {
    value: "photos",
    label: "الصور",
    children: [{ value: "trips", label: "الرحلات" }],
  },
  { value: "notes", label: "ملاحظات" },
]

function UiSdkTreeRtl() {
  return (
    <div dir="rtl" className="w-full max-w-xs">
      <Tree
        aria-label="الملفات"
        items={items}
        defaultExpanded={["documents"]}
        defaultValue="reports"
      />
    </div>
  )
}

export { UiSdkTreeRtl }
```

## Keyboard

| Key | Action |
| --- | --- |
| `↓` `↑` | Moves to the next or previous visible item. |
| `→` | Opens a branch, or moves to its first child. |
| `←` | Closes a branch, or moves to its parent. |
| `Home` `End` | Moves to the first or last visible item. |
| `Alt` `→` | Opens a branch and everything inside it. |
| `*` | Opens every branch beside the focused one. |
| `Enter` | Selects the item and runs its action, or follows its link. |
| `Space` | Checks the item when the tree is checkable, otherwise selects it. |
| `Shift` `↓` | Extends a multiple selection. |
| `⌘` `A` | Selects every visible item in multiple mode. |

## Accessibility

The tree follows the WAI-ARIA tree view pattern with one tab stop. Tab lands on the current page, the selected item or the first item. Typing a letter moves to the next item that starts with it. While a slow `onAction` runs, screen readers hear `pendingAnnouncement` after a second, so set it in your page’s language.

Groups open and close by height only when motion is allowed. On touch screens rows are taller, and in forced colors mode the selected and current rows get an outline.

## API reference

Each part wraps the UI SDK part of the same name and accepts all of its props, including `render` and a `className` that can be a function of the part’s state.

### Tree

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `TreeItemData[]` | – | The whole hierarchy. Without children, the tree renders styled rows from it. |
| `paths` | `string[]` | – | File paths. Folders become branches; a path ending in / is an empty folder. |
| `mode` | `"single" \| "multiple" \| "none"` | `"single"` |  |
| `value` | `string \| string[] \| null` | – |  |
| `defaultValue` | `string \| string[] \| null` | – |  |
| `onValueChange` | `(value, details) => void` | – |  |
| `activation` | `"automatic" \| "manual"` | `"manual"` |  |
| `expanded` | `string[]` | – |  |
| `defaultExpanded` | `string[]` | – |  |
| `onExpandedChange` | `(expanded, details) => void` | – |  |
| `expandOnPress` | `boolean` | `true` |  |
| `checkable` | `boolean` | `false` |  |
| `checked` | `string[]` | – |  |
| `defaultChecked` | `string[]` | – |  |
| `onCheckedChange` | `(checked, details) => void` | – |  |
| `checkStrategy` | `"leaf" \| "trunk" \| "branch" \| "independent"` | `"leaf"` |  |
| `onAction` | `(value, details) => unknown` | – | Runs on press or Enter. Return a promise to mark the item pending. |
| `pendingAnnouncement` | `string` | – |  |
| `loadChildren` | `(value, { signal }) => Promise<unknown>` | – |  |
| `current` | `string \| null` | – |  |
| `disabled` | `boolean` | `false` |  |
| `hiddenUntilFound` | `boolean` | `false` |  |
| `keepMounted` | `boolean` | `false` |  |
| `variant` | `"default" \| "lines"` | `"default"` |  |
| `size` | `"default" \| "sm"` | `"default"` |  |
| `controller` | `TreeController` | – | From createTreeController(). |

| Attribute | Description |
| --- | --- |
| `data-slot="tree"` | Target the tree in CSS. |
| `data-variant` | The variant: default or lines. |
| `data-size` | The size: default or sm. |
| `--tree-row-height` | The height of each row, set by size. |
| `--tree-indent` | How far each level indents. |

### TreeItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | – | Unique in the tree. |
| `branch` | `boolean` | – | Set it for a branch whose children load later. |
| `disabled` | `boolean` | `false` |  |
| `label` | `string` | – | The text typeahead matches, when the label isn’t plain text. |
| `render` | `ReactElement \| (props, state) => ReactElement` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="tree-item"` | Target an item in CSS. It holds focus. |
| `data-level` | The depth, from 1. |
| `data-branch` | Present on a branch. |
| `data-expanded` | Present while a branch is open. |
| `--tree-level` | The depth, for indenting a flat list. |

### TreeItemLabel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| (props, state) => ReactElement` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="tree-label"` | Target a row in CSS. |
| `data-selected` | Present while the item is selected. |
| `data-current` | Present on the current page’s item. |
| `data-expanded` | Present while a branch is open. |
| `data-pending` | Present while the item’s onAction runs. |
| `data-disabled` | Present when the item is disabled. |

### TreeItemIndicator

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode \| (state) => ReactNode` | – | Defaults to a chevron, a spinner while loading and a retry icon after a failure. |

| Attribute | Description |
| --- | --- |
| `data-slot="tree-indicator"` | Target the chevron in CSS. |
| `data-loading` | Present while children load. |
| `data-failed` | Present after children failed to load. |

### TreeItemCheckbox

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode \| (state) => ReactNode` | – | Defaults to a check, or a dash when mixed. |

| Attribute | Description |
| --- | --- |
| `data-slot="tree-checkbox"` | Target the checkbox in CSS. |
| `data-checked` | Present while checked. |
| `data-indeterminate` | Present while a branch is partly checked. |

### TreeGroup

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `keepMounted` | `boolean` | – |  |
| `hiddenUntilFound` | `boolean` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="tree-group"` | Target a group in CSS. |
| `data-open` | Present while open. |
| `--tree-group-height` | The group’s height while it animates. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt

## Related

- [Avatar](https://hextaui.com/docs/avatar) - User photos with an initials fallback, status badges and stacked groups that collapse into a count.
- [Badge](https://hextaui.com/docs/badge) - Status labels with colored dots, removable tags that slide closed and counts that roll to their new value.
- [Card](https://hextaui.com/docs/card) - A surface for grouping content, with three variants, edge-to-edge media, concentric radii and whole-card links.
- [Chart](https://hextaui.com/docs/chart) - Recharts charts with themed colors, a tooltip and legend that read labels from one config, and keyboard navigation with a visible focus ring.
- [Data table](https://hextaui.com/docs/data-table) - A table for real data, with sorting, search, row selection, pinned columns, a sticky header and pagination.
- [Item](https://hextaui.com/docs/item) - A row of media, text and actions for lists, settings and pickers, with a grouped surface and a hover highlight that glides between rows.
