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

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/tree-select`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { TreeSelect } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/tree-select

> Select-style field whose overlay is an expandable, selectable tree — single or multiple selection returning node id(s).

## When to use it

Use **TreeSelect** to choose from a hierarchy that is browsed as a whole — a folder, a department, a category tree — where expanding several branches at once helps. When the choice is a strict drill-down path, **Cascader**'s columns fit better; for a flat list use **Select** or **MultiSelect**.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `defaultExpanded` | `string[]` | no | — | Initial expanded ids in uncontrolled mode. |
| `defaultOpen` | `boolean` | no | — | Initial open state in uncontrolled mode. |
| `defaultValue` | `string \| string[]` | no | — | Initial selection in uncontrolled mode. |
| `disabled` | `boolean` | no | — | Disable the whole control. |
| `expanded` | `string[]` | no | — | Controlled expanded branch ids. |
| `invalid` | `boolean` | no | — | Mark the field invalid — danger border plus `aria-invalid`. |
| `items` | `TreeNode[]` | yes | — | The node forest rendered in the overlay tree. |
| `onExpandedChange` | `((expanded: string[]) => void)` | no | — | Fires with the next expanded-id array. |
| `onOpenChange` | `((open: boolean) => void)` | no | — | Fires when the overlay opens or closes. |
| `onValueChange` | `((value: string \| string[]) => void)` | no | — | Fires with the next selection, matching `selectionMode`. |
| `open` | `boolean` | no | — | Controlled open state of the overlay. |
| `placeholder` | `string` | no | `Select` | Text shown when nothing is selected. |
| `selectionMode` | `enum` | no | `single` | `single` selects one id; `multiple` toggles a set. |
| `separator` | `string` | no | `,` | Separator between labels in the trigger (multiple mode). |
| `size` | `enum` | no | `md` | Field height: `sm` (32) · `md` (36, default) · `lg` (40). |
| `style` | `CSSProperties` | no | — | — |
| `value` | `string \| string[]` | no | — | Controlled selection — a string (single) or string[] (multiple). |

## Examples

### Basics

The trigger shows the selected node's label; opening it reveals a `TreeView` you can expand and pick from. In single mode a pick closes the overlay. The value is the node id; controllable via `value` / `defaultValue` / `onValueChange`.

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

const ITEMS: TreeNode[] = [
  {
    id: "eng",
    label: "Engineering",
    children: [
      { id: "fe", label: "Frontend" },
      { id: "be", label: "Backend" },
      { id: "infra", label: "Infrastructure" },
    ],
  },
  {
    id: "design",
    label: "Design",
    children: [
      { id: "brand", label: "Brand" },
      { id: "product", label: "Product" },
    ],
  },
];

export default function Basics() {
  const [value, setValue] = useState<string | string[]>("fe");

  return (
    <div style={{ width: 260 }}>
      <TreeSelect
        aria-label="Team"
        items={ITEMS}
        value={value}
        onValueChange={setValue}
        defaultExpanded={["eng"]}
      />
    </div>
  );
}
```

### Multiple selection

Set `selectionMode="multiple"` to accumulate a `string[]` of ids — the overlay stays open as you toggle nodes, and the trigger lists every chosen label.

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

const ITEMS: TreeNode[] = [
  {
    id: "docs",
    label: "Documents",
    children: [
      { id: "specs", label: "Specs" },
      { id: "adrs", label: "ADRs" },
    ],
  },
  {
    id: "media",
    label: "Media",
    children: [
      { id: "img", label: "Images" },
      { id: "video", label: "Video" },
    ],
  },
];

export default function Multiple() {
  const [value, setValue] = useState<string | string[]>(["specs", "img"]);

  return (
    <div style={{ width: 260 }}>
      <TreeSelect
        aria-label="Scopes"
        items={ITEMS}
        selectionMode="multiple"
        value={value}
        onValueChange={setValue}
        defaultExpanded={["docs", "media"]}
      />
    </div>
  );
}
```

## Do & don't

**Do**

- Give the trigger an accessible name via a `Field` label or `aria-label`.
- Provide `defaultExpanded` so the likely branch opens on first use.
- Give each node a `textValue` when its `label` isn't plain text.
- Read node id(s) from `onValueChange` — a string in single mode, a `string[]` in multiple.

**Don't**

- Don't use it for a strict path down one branch — Cascader's columns are clearer.
- Don't leave the trigger unlabelled when no visible field label wraps it.
- Don't collapse everything by default when the useful nodes are buried deep.
- Don't duplicate node ids — they key both selection and expansion.

## Accessibility

**Keyboard**

| Keys | Action |
| --- | --- |
| `Enter / Space` | Open the overlay, or select the focused node. |
| `Arrow Up / Down` | Move between visible nodes in the tree. |
| `Arrow Right / Left` | Expand / collapse the focused branch. |
| `Esc` | Close the overlay without changing the selection. |

**Notes**

- The overlay is a Radix Popover; the tree is the shared `TreeView`, which implements the full ARIA tree keyboard model (arrows, Home/End, type-ahead, `*`).
- The tree is a `role="tree"` of `role="treeitem"` nodes with `aria-expanded`, `aria-selected`, and `aria-level`; multiple mode adds `aria-multiselectable`.
- `invalid` sets `aria-invalid` on the trigger; the trigger exposes `aria-expanded` for its open state.

## Related

`cascader`, `tree-view`, `select`, `multi-select`

---

© 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/
