# Tree Select URL: /docs/web/components/tree-select A select dropdown backed by a tree. Single mode renders like Select; multi mode renders like Tag Select. Hierarchical search, expand-all and parent-cascade checking come from the embedded Tree View. **Demo:** ```tsx 'use client'; // @custom-demo import { useMemo, useState, type ReactNode } from 'react'; import { Briefcase, Building2, ChevronDown, Coins, Cpu, Database, Folder, Megaphone, Settings2, Users } from 'lucide-react'; import { TreeSelect, type TreeSelectDefaultExpanded } from '@docyrus/ui/components/tree-select'; import { type TreeViewItem } from '@docyrus/ui/components/tree-view'; import { Button } from '@docyrus/ui/primitives/ui/button'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@docyrus/ui/primitives/ui/select'; import { Switch } from '@docyrus/ui/primitives/ui/switch'; import { cn } from '@docyrus/ui/primitives/lib/utils'; const DEPARTMENT_TREE: TreeViewItem[] = [ { id: 'engineering', name: 'Engineering', type: 'group', children: [ { id: 'frontend', name: 'Frontend', type: 'team', children: [{ id: 'web', name: 'Web', type: 'leaf' }, { id: 'mobile', name: 'Mobile', type: 'leaf' }, { id: 'design-systems', name: 'Design Systems', type: 'leaf' }] }, { id: 'backend', name: 'Backend', type: 'team', children: [{ id: 'api', name: 'API Platform', type: 'leaf' }, { id: 'data', name: 'Data Platform', type: 'leaf' }, { id: 'infra', name: 'Infrastructure', type: 'leaf' }] } ] }, { id: 'product', name: 'Product', type: 'group', children: [{ id: 'design', name: 'Design', type: 'leaf' }, { id: 'research', name: 'Research', type: 'leaf' }, { id: 'analytics', name: 'Analytics', type: 'leaf' }] }, { id: 'gtm', name: 'Go-to-market', type: 'group', children: [{ id: 'marketing', name: 'Marketing', type: 'leaf' }, { id: 'sales', name: 'Sales', type: 'leaf' }, { id: 'cs', name: 'Customer Success', type: 'leaf' }] }, { id: 'ops', name: 'Operations', type: 'group', children: [{ id: 'finance', name: 'Finance', type: 'leaf' }, { id: 'people', name: 'People Ops', type: 'leaf' }] } ]; const ICON_MAP: Record = { group:
Default expanded
{ if (multiple) { setMultiValue(Array.isArray(next) ? next : []); return; } setSingleValue(typeof next === 'string' ? next : null); }} multiple={multiple} leafOnly={leafOnly} defaultExpanded={defaultExpanded} showBreadcrumb={showBreadcrumb} disabled={disabled} maxHeight="320px" getIcon={showIcons ? renderIcon : undefined} searchPlaceholder="Search departments..." />

{multiple ? leafOnly ? 'Click a folder row to expand it; tick checkboxes to add leaf departments.' : 'Tick checkboxes to add departments. Ticking a parent cascades to its descendants.' : leafOnly ? 'Click a folder row to expand it; only leaf rows commit a value.' : 'Click a row to pick a single department. The dropdown closes on selection.'}

          value = {valueLabel}
        
); } ``` ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/ui-tree-select ``` **Dependencies:** - [lucide-react](https://www.npmjs.com/package/lucide-react) ## Overview `TreeSelect` is a [Popover](/docs/web/components/popover)-anchored picker whose dropdown body is the [`TreeView`](/docs/web/components/tree-view) component. Pass `multiple` to switch between two trigger shapes that mirror the existing form-field family: | Mode | Trigger | Dropdown | |------|---------|----------| | `multiple={false}` (default) | One-line, [Select](/docs/web/components/select)-style row | TreeView without checkboxes — clicking a row picks it and closes the popover | | `multiple` | Wrap-flowing badges with per-badge `×` — same shape as [Tag Select](/docs/web/components/form-fields) | TreeView with checkboxes — ticking a parent cascades to its descendants | The dropdown's search input, expand/collapse-all menu and keyboard support come straight from `TreeView`, so the same `TreeViewItem[]` you'd pass to a plain tree works here without any reshaping. ## Usage ```tsx import { TreeSelect } from "@docyrus/ui/components/tree-select"; import { type TreeViewItem } from "@docyrus/ui/components/tree-view"; const data: TreeViewItem[] = [ { id: "engineering", name: "Engineering", type: "group", children: [ { id: "frontend", name: "Frontend", type: "team" }, { id: "backend", name: "Backend", type: "team" } ] }, { id: "product", name: "Product", type: "group" } ]; export function Example() { const [value, setValue] = useState(null); return ( setValue(typeof next === "string" ? next : null)} placeholder="Select a department..." /> ); } ``` ### Multi-select Set `multiple` and treat `value` as an array of ids. The trigger renders one removable badge per selection. ```tsx const [value, setValue] = useState([]); setValue(Array.isArray(next) ? next : [])} /> ``` ### Leaf-only selection Set `leafOnly` to restrict the value to leaf nodes only: - **Single mode** — clicking a folder row expands / collapses it instead of committing a value. Only leaf rows commit. - **Multi mode** — checking a folder cascades to its **leaf** descendants only; the folder id itself never enters `value`. Clicking a folder row (outside the checkbox) also expands / collapses it. This relies on `TreeView`'s new `expandFolderOnRowClick` flag, which `TreeSelect` enables automatically when `leafOnly` is on. The default (`leafOnly={false}`) treats folders as selectable values and makes a checked folder add the folder id **and** every descendant id. ```tsx setValue(Array.isArray(next) ? next : [])} /> ``` ### Default expansion `defaultExpanded` controls which folders are open each time the dropdown mounts. The default — `'selected'` — auto-expands the ancestor chain of every currently-selected item so the value is always visible on reopen. ```tsx // Always show the full tree. // Start fully collapsed. // Open exactly these folders. ``` | Value | Behaviour | |-------|-----------| | `'selected'` (default) | Expand ancestors of items in `value`. No-op when `value` is empty. | | `'all'` | Expand every folder. | | `'none'` | Start fully collapsed. | | `string[]` | Expand exactly these ids. | The popover unmounts its content on close, so the strategy is re-applied every time the dropdown opens — `'selected'` always reflects the current value, not the initial one. ### Trigger breadcrumb Set `showBreadcrumb` to render the ancestor path on the trigger for each selected item. Ancestor names render in muted text; the leaf name uses the foreground color. `renderSelected` still wins when both are supplied. ```tsx setValue(typeof next === "string" ? next : null)} /> // Trigger shows: Engineering › Frontend › Web ``` In multi mode the breadcrumb is repeated **per badge**, so each chip carries its own ancestor chain. ### Custom row icons Forward `getIcon` (and / or `iconMap`) directly to the underlying TreeView. The same `(item, depth) => ReactNode` signature applies. ```tsx import { Building2, Folder, Briefcase } from "lucide-react"; const iconMap = { group: , team: , leaf: }; ``` ### Custom trigger badges `renderSelected` controls how each selected item appears inside the trigger. Use it to inject icons, color swatches, or any custom shape — the per-badge remove button (`×`) is added automatically in multi mode. ```tsx ( {item.name} )} /> ``` ### Controlled open state `TreeSelect` is uncontrolled by default — pass `open` + `onOpenChange` to take over the popover state (useful when driving the picker from a toolbar button or external keyboard shortcut). ```tsx const [open, setOpen] = useState(false); ``` ## API Reference | Prop | Type | Default | Description | |------|------|---------|-------------| | `data` | `TreeViewItem[]` | — | Tree data shown in the dropdown. Reuses the [TreeView](/docs/web/components/tree-view) data shape. | | `value` | `string \| string[] \| null` | — | Currently selected id(s). String in single mode, array in multi mode. | | `onValueChange` | `(value: string \| string[] \| null) => void` | — | Fired when the selection changes. Receives `string \| null` in single mode, `string[]` in multi mode. | | `multiple` | `boolean` | `false` | Enable multi-select mode (badge trigger + checkbox dropdown). | | `placeholder` | `string` | `'Select...'` / `'Select items...'` | Placeholder shown when nothing is selected. Falls back to a localized default. | | `disabled` | `boolean` | `false` | Disable the trigger button. | | `className` | `string` | — | Class applied to the trigger button. | | `id` | `string` | — | Trigger button id — pair with a `