# Tree Select

> A React tree select: a field that opens a searchable tree of categories, folders or locations, for one value or many as removable tags.

- Type: React component (Inputs)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add tree-select`
- Docs: https://wingo-ui.com/components/tree-select

## Interaction

Click or tap the field and a tree opens under it with the search row focused. Type to filter: matches keep their parents, the path to them opens and the matched letters turn bold. Click a row to pick it and the list closes; clicking a folder only opens it unless selectableFolders is on (then the chevron opens it). With multiple, each row has a checkbox and the list stays open: checking a folder checks its whole branch, a folder turns to a dash when only some of its children are checked, the trigger shows removable tags up to maxTagCount and then +N, and the footer counts and clears. Keyboard: on the closed field Enter, Space, ArrowDown or ArrowUp open it and a letter opens it with that letter in the search; Backspace removes the last tag (or clears a single value with clearable). In the search ArrowDown or Enter move into the tree. In the tree ArrowDown and ArrowUp move (ArrowUp on the first row returns to the search), ArrowRight opens a folder or steps into it, ArrowLeft closes it or jumps to the parent, Home and End jump, typing letters jumps to the next row that starts with them, Enter picks a row (or opens a folder), Space picks or checks, Escape closes and puts focus back on the field, and Tab moves on from the field. On a phone the list opens as a bottom sheet with the search pinned under the title and 44px rows; with multiple a Done button closes it.

## Installation

```bash
npx wingo-ui@latest add tree-select
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/tree-select` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/tree-select.tsx`

npm dependencies: `npm i @radix-ui/react-popover motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Tree View](https://wingo-ui.com/components/tree-view) ([Markdown](https://wingo-ui.com/components/tree-view.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Tag](https://wingo-ui.com/components/tag) ([Markdown](https://wingo-ui.com/components/tag.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { useState } from "react"
import TreeSelect, { type TreeSelectNode } from "@/components/ui/tree-select"

const categories: TreeSelectNode[] = [
  {
    id: "electronics",
    label: "Electronics",
    children: [
      { id: "phones", label: "Phones", children: [{ id: "iphone", label: "iPhone" }, { id: "android", label: "Android" }] },
      { id: "laptops", label: "Laptops" },
    ],
  },
  { id: "home", label: "Home & Kitchen", children: [{ id: "furniture", label: "Furniture" }] },
]

export function ProductCategory() {
  const [category, setCategory] = useState("android")
  const [filters, setFilters] = useState<string[]>(["phones"])
  return (
    <div className="grid gap-4">
      <TreeSelect label="Category" data={categories} value={category} onValueChange={setCategory} showPath />
      <TreeSelect label="Show products in" data={categories} multiple value={filters} onValueChange={setFilters} clearable />
    </div>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `data` | `TreeSelectNode[]` |  | The tree: { id, label, textValue?, icon?, description?, disabled?, children? }. Ids must be unique across the whole tree; a label that is not a string needs textValue for the search, the tags and the path. |
| `multiple?` | `boolean` | `false` | Several values as removable tags, with checkboxes in the tree. The value becomes a string[]. |
| `value?` | `string \| string[]` |  | The chosen id ("" is nothing), or ids with multiple (controlled). Pair it with onValueChange. |
| `defaultValue?` | `string \| string[]` | `""` | The starting value while uncontrolled ([] with multiple). A form reset returns to it. |
| `onValueChange?` | `(value: string) => void \| (value: string[]) => void` |  | Called with the new id, or the new list with multiple. |
| `placeholder?` | `string` | `"Select an option"` | Shown while nothing is chosen. Also titles the phone sheet when there is no label. |
| `label?` | `ReactNode` | `""` | The label above the field; plain text also names the tree and titles the phone sheet. |
| `description?` | `ReactNode` | `""` | Helper text under the field, linked with aria-describedby. |
| `error?` | `ReactNode` | `""` | Replaces the description with an error and marks the field invalid. |
| `required?` | `boolean` | `false` | Adds the asterisk and aria-required; with name, an empty field blocks the form submit. |
| `searchable?` | `boolean` | `true` | A search row on top of the tree. Matches keep their parents, the path to them opens and the matched letters turn bold; accents never matter. |
| `selectableFolders?` | `boolean` | `false` | Folders can be picked too (single), or checked on their own (multiple without cascade). Off: a folder row only opens and closes. |
| `cascade?` | `boolean` | `true` | Multiple only: checking a folder checks its branch and a fully checked branch checks its folder. Off: every row checks on its own. |
| `checkStrategy?` | `"all" \| "parent" \| "child"` | `"parent"` | Multiple with cascade: what the value holds. parent keeps the topmost fully checked folder instead of its children, child keeps only the leaves, all keeps every checked id. |
| `defaultExpanded?` | `boolean \| string[]` | `false` | Folders open on first render: true opens every folder, or pass ids. The path to the chosen value always opens with the list. |
| `showPath?` | `boolean` | `false` | The field shows the whole path (Electronics / Phones / Android). The folders truncate first so the picked name stays readable; tags keep the path in their title. |
| `pathSeparator?` | `string` | `" / "` | Between the names of the path. |
| `clearable?` | `boolean` | `false` | An x in the field that clears the value. It only shows while something is chosen. |
| `maxTagCount?` | `number` | `2` | Multiple only: tags shown before a +N tag (its title lists the rest). 0 shows every tag. |
| `wrapTags?` | `boolean` | `false` | Multiple only: tags wrap onto more lines and the field grows, instead of one clipped row. |
| `guides?` | `boolean` | `true` | Vertical indent guides in the tree. |
| `folderIcons?` | `boolean` | `false` | Folder and file icons for nodes that have no icon of their own. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Field height 32 / 40 / 48px (44px on touch for md), the tree rows and the tags follow. |
| `variant?` | `"soft" \| "outline" \| "underline" \| "ghost"` | `"soft"` | The field look, the same as every input and select. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner shape, scaled with the size like the button (md is 8 / 10 / 12px). Tags follow it. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | The checkboxes, the picked row, the tags and the sheet's Done button. |
| `color?` | `string` | `""` | Any CSS color; replaces the tone. Leave empty to use the tone. |
| `invalid?` | `boolean` | `false` | The red ring and aria-invalid without a message (error sets it too). |
| `disabled?` | `boolean` | `false` | Dims the field and blocks it. |
| `readOnly?` | `boolean` | `false` | Shows the value and keeps it focusable, but the list does not open and tags cannot be removed. |
| `responsive?` | `boolean` | `true` | A bottom sheet below 768px, with the search pinned under the title, 44px rows and (multiple) a Done button. |
| `sheetTitle?` | `string` | `""` | The phone sheet's title. Default: the label's text, then aria-label, then the placeholder. |
| `side?` | `"top" \| "bottom"` | `"bottom"` | Where the popover opens; it flips when there is no room. |
| `align?` | `"start" \| "center" \| "end"` | `"start"` | Popover alignment against the field. |
| `maxHeight?` | `number` | `360` | The popover's maximum height in px (at least 160); it never grows past the room on screen. |
| `name?` | `string` | `""` | A hidden input per value with this name, so the field submits with a form (FormData.getAll(name)). |
| `id?` | `string` |  | The trigger's id; the label points to it. |
| `leftIcon?` | `ReactNode` |  | An icon at the start of the field. |
| `open?` | `boolean` |  | Controlled open state. Pair it with onOpenChange. |
| `defaultOpen?` | `boolean` | `false` | Starts with the list (or the sheet) open. Pass open and onOpenChange to control it. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when the list opens or closes. |
| `labels?` | `Partial<TreeSelectLabels>` |  | Every string: { search: "Search", clearSearch: "Clear search", noResults: "No matches", clear: "Clear", done: "Done", remove: "Remove", more: (n) => "+n", selected: (n) => "n selected", tree: "Options" }. |
| `container?` | `HTMLElement \| null` |  | Portal target for the popover and the sheet (default document.body). |
| `fieldProps?` | `Partial<FieldProps>` |  | Anything else for the Field wrapper: optional, labelAction, orientation, hideLabel. |
| `classNames?` | `Partial<Record<"field" \| "label" \| "description" \| "error" \| "box" \| "trigger" \| "value" \| "path" \| "placeholder" \| "tags" \| "tag" \| "overflow" \| "clear" \| "chevron" \| "content" \| "search" \| "tree" \| "footer", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Merged onto the Field root when there is a label, description or error, otherwise onto the box. |
| `ref?` | `RefObject<HTMLButtonElement \| null> or (node) => void` |  | The trigger button; other button props (onBlur, aria-*, data-*, form) land there too. |

## Parts

### TreeSelectNode (type)

One node of data: { id, label, textValue?, icon?, description?, disabled?, children? }.

### TreeSelectProps (type)

The props, with value typed as a string or, with multiple, a string[].

### TreeSelectLabels (type)

The shape of the labels prop.

### DEFAULT_TREE_SELECT_LABELS (constant)

The English labels; spread them to translate a few strings.

### getTreeSelectPath (function)

The names from the root down to a node: getTreeSelectPath(data, "android") gives ["Electronics", "Phones", "Android"]. Handy for a summary or a breadcrumb.

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `data` | `TreeSelectNode[]` |  | The tree. |
| `id` | `string` |  | The node; an unknown id gives []. |

### treeSelectText (function)

A node's plain text: textValue, else a string label, else the id.

## Examples

- **With the path**: showPath shows Electronics / Laptops / Windows laptops and clearable adds an x.
- **Several categories**: multiple turns rows into checkboxes and values into tags; a folder checks its whole branch.
- **Check strategies**: The same picks reported three ways: every id, the topmost folders, or the leaves only.
- **Pick a department or a team**: selectableFolders lets a folder be the value; the chevron opens it.
- **Wrapping tags**: checkStrategy="child" keeps cities only, maxTagCount={0} and wrapTags show every tag.
- **Independent checks**: cascade={false} with selectableFolders: each folder is its own choice, in the info tone.
- **Sizes and states**: sm, md and lg line up with every field and button; disabled and invalid below.
- **In a form**: required, a validation error and a hidden input named category.

## See also

- [Tree View](https://wingo-ui.com/components/tree-view) ([Markdown](https://wingo-ui.com/components/tree-view.md))
- [Multi Select](https://wingo-ui.com/components/multi-select) ([Markdown](https://wingo-ui.com/components/multi-select.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Combobox](https://wingo-ui.com/components/combobox) ([Markdown](https://wingo-ui.com/components/combobox.md))
- [Tag Input](https://wingo-ui.com/components/tag-input) ([Markdown](https://wingo-ui.com/components/tag-input.md))

---

Canonical: https://wingo-ui.com/components/tree-select
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
