<!-- Protocore Design System — TreeView -->
# TreeView

- **Category:** Data Display (`data-display`)
- **Slug:** `data-display/tree-view`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { TreeView } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/data-display/tree-view

> A disclosure tree of expandable nodes with hairline indent rails, single or multiple selection, and the full APG tree keyboard model.

## When to use it

Reach for **TreeView** when data is genuinely **hierarchical** and users need to expand, collapse, and select nodes across depths — a file browser, an infrastructure tree, a nested category picker. For a flat list of collapsible sections, use **Accordion**. For read-only nested JSON, use **JsonViewer**. Keep node `id`s stable and unique — they are the selection and expansion keys.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `aria-label` | `string` | no | — | Accessible name for the tree (required for a labelled `role="tree"`). |
| `className` | `string` | no | — | — |
| `defaultExpanded` | `string[]` | no | — | Initial expanded ids in uncontrolled mode. |
| `defaultValue` | `string \| string[]` | no | — | Initial selection in uncontrolled mode. |
| `expanded` | `string[]` | no | — | Controlled set of expanded branch ids. |
| `items` | `TreeNode[]` | yes | — | The node forest to render. |
| `onExpandedChange` | `((expanded: string[]) => void)` | no | — | Fires with the next expanded-id array. |
| `onSelect` | `((id: string) => void)` | no | — | Fires with the id each time a node is activated (before selection settles). |
| `onValueChange` | `((value: string \| string[]) => void)` | no | — | Fires with the next selection, matching the `selectionMode` shape. |
| `selectionMode` | `enum` | no | `single` | `single` (default) selects one id; `multiple` toggles a set. |
| `style` | `CSSProperties` | no | — | — |
| `value` | `string \| string[]` | no | — | Controlled selection — a string in single mode, a string[] in multiple mode. |

## Examples

### Basics

Pass a recursive `items` array of `{ id, label, children? }`. Nodes with `children` render a chevron toggle; expansion is controllable via `expanded` / `defaultExpanded`, selection via `value` / `defaultValue`.

```tsx
import { TreeView, type TreeNode } from "@protocore/pds";

const ITEMS: TreeNode[] = [
  {
    id: "infra",
    label: "infra",
    children: [
      { id: "vpc", label: "vpc.tf" },
      { id: "s3", label: "s3.tf" },
      { id: "iam", label: "iam.tf" },
    ],
  },
  {
    id: "services",
    label: "services",
    children: [
      {
        id: "api",
        label: "api",
        children: [
          { id: "handler", label: "handler.ts" },
          { id: "router", label: "router.ts" },
        ],
      },
      { id: "worker", label: "worker" },
    ],
  },
  { id: "readme", label: "README.md" },
];

export default function Demo() {
  return (
    <div style={{ width: 280 }}>
      <TreeView
        aria-label="Repository"
        items={ITEMS}
        defaultExpanded={["infra", "services"]}
        defaultValue="vpc"
      />
    </div>
  );
}
```

### Multiple selection

Set `selectionMode="multiple"` to toggle a set of ids. The tree advertises `aria-multiselectable` and each node carries its own `aria-selected`.

```tsx
import { TreeView, Text, type TreeNode } from "@protocore/pds";
import { useState } from "react";

const ITEMS: TreeNode[] = [
  {
    id: "eu-central-1",
    label: "eu-central-1",
    children: [
      { id: "eu-a", label: "eu-central-1a" },
      { id: "eu-b", label: "eu-central-1b" },
      { id: "eu-c", label: "eu-central-1c" },
    ],
  },
  {
    id: "us-east-1",
    label: "us-east-1",
    children: [
      { id: "us-a", label: "us-east-1a" },
      { id: "us-b", label: "us-east-1b" },
    ],
  },
];

export default function Demo() {
  const [value, setValue] = useState<string[]>(["eu-a", "eu-b"]);

  return (
    <div style={{ width: 300 }}>
      <TreeView
        aria-label="Availability zones"
        items={ITEMS}
        selectionMode="multiple"
        defaultExpanded={["eu-central-1", "us-east-1"]}
        value={value}
        onValueChange={(v) => setValue(v as string[])}
      />
      <Text mono size="sm" color="muted" style={{ marginTop: "var(--pds-space-3)" }}>
        selected: {value.length ? value.join(", ") : "none"}
      </Text>
    </div>
  );
}
```

## Do & don't

**Do**

- Give every node a stable, unique `id`.
- Pass an `aria-label` to the tree so `role="tree"` is named.
- Provide a `textValue` when a node's `label` isn't plain text, so type-ahead still works.
- Control `expanded` when the open/closed state must survive navigation.

**Don't**

- Don't use a tree for a flat list — a list, RadioGroup, or Accordion reads faster.
- Don't put interactive controls inside a node label; the row itself is the click target.
- Don't reuse `id`s across nodes — selection and focus will collide.
- Don't ship emoji for the expand glyph; the chevron is a monochrome inline SVG for a reason.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Arrow Down / Up` | Move to the next / previous visible node. |
| `Arrow Right` | Expand a collapsed branch, or step into its first child if already open. |
| `Arrow Left` | Collapse an open branch, or step to the parent node. |
| `Home / End` | Move to the first / last visible node. |
| `Enter / Space` | Select (or toggle, in multiple mode) the focused node. |
| `Type a letter` | Jump to the next node whose label starts with it. |
| `*` | Expand every sibling branch at the current level. |

**Notes**

- Built to the WAI-ARIA tree pattern: `role="tree"` over `role="treeitem"` with `aria-expanded`, `aria-selected`, `aria-level`, `aria-setsize`, and `aria-posinset`; nested children live in a `role="group"`.
- Roving tabindex — one node is tabbable at a time, so the tree is a single Tab stop.
- Each node is named by its own label (not the concatenated subtree), so screen readers announce the node, not its descendants.

## Related

`accordion`, `sidebar`, `json-viewer`, `definition-list`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
