TreeSelect
Pick a single value from hierarchical data — an always-expanded tree in a Popover where only leaves are selectable.
TreeSelect lets the user pick one value from hierarchical data. It pairs a text trigger (like Select) with an always-expanded tree in a Popover: top-level rows sit flush, their descendants are connected by indent guide lines. Only leaves are selectable — a node with children is an organizational branch, not a value — and picking a leaf closes the popover. A search field can filter the tree.
import { TreeSelect } from '@42/ui-react/tree-select';
<TreeSelect
data={[
{ value: 'srcs', label: 'srcs', children: [
{ value: 'main.c', label: 'main.c' },
]},
{ value: 'Makefile', label: 'Makefile' },
]}
placeholder="Pick a file"
/>For multiple values (with cascading folder selection and pills), reach for TreeMultiSelect.
Playground
Component
Dir
Presets
Props
Code
<Component placeholder="Pick a file" clearSectionMode="replace" size="md"/>Data
data is a tree of nodes — each { value, label, disabled?, children? }. A node with children is a branch: it organizes the tree but cannot be selected. A node without children is a leaf — the only selectable kind. value must be unique across the tree.
const data = [
{ value: 'builtins', label: 'builtins', children: [
{ value: 'ft_cd', label: 'ft_cd' },
{ value: 'ft_echo', label: 'ft_echo' },
]},
{ value: 'parsing', label: 'parsing', children: [
{ value: 'lexer', label: 'lexer' },
]},
];Controlled
value is the selected leaf value, or null when nothing is picked; onChange fires with the value (or null when cleared). Picking a leaf closes the popover.
'use client';
import { useState } from 'react';
import { TreeSelect } from '@42/ui-react/tree-select';
export function FilePicker({ tree }) {
const [value, setValue] = useState<string | null>(null);
return <TreeSelect data={tree} value={value} onChange={setValue} clearable />;
}Clearable
With clearable, the chevron becomes a clear button once a value is set.
Searchable
A search field at the top of the popover filters the tree, keeping the ancestors of any match. The popover opens with focus in the field, and ↓ moves into the first leaf.
Start & end slots
startSlot and endSlot place content at the trigger's inline-start / inline-end with the same slot layout as Input. endSlot renders before the built-in clear button and chevron.
Custom rendering
Pass renderItem to fill each node's content yourself. It receives the node ({ value, label, disabled?, children? }) and its live { selected, leaf, disabled, depth } state. TreeSelect keeps the row — treeitem role, indent rails, roving focus, click — and the callback fills the content after the rails, including any selected affordance. Read state.leaf to tell organizational branches from selectable leaves. Define the function in a "use client" module so it survives the server → client boundary.
'use client';
import { Check, FileCode, Folder } from 'lucide-react';
const renderNode = (node, { leaf, selected }) => (
<span className="flex flex-1 items-center gap-2">
{leaf ? <FileCode className="size-4" /> : <Folder className="size-4 text-(--c-solid)" />}
<span className="flex-1 truncate text-start">{node.label}</span>
{selected && <Check className="size-4 text-(--c-solid)" />}
</span>
);
<TreeSelect data={tree} renderItem={renderNode} />Typed payload
Nodes can carry an arbitrary, typed payload ({ value, label, payload?, children? }), read back as node.payload in renderItem — inferred from data, and undefined on nodes that omit it.
'use client';
import type { TreeNodeData } from '@42/ui-react/tree-select';
type FileMeta = { loc: number };
const tree: TreeNodeData<FileMeta>[] = [
{ value: 'srcs', label: 'srcs', children: [
{ value: 'main.c', label: 'main.c', payload: { loc: 12 } },
]},
];
// node.payload is inferred as FileMeta | undefined
const renderNode = (node) => (
<span className="flex flex-1 items-center justify-between gap-2">
{node.label}
{node.payload && <span className="text-xs">{node.payload.loc} LOC</span>}
</span>
);
<TreeSelect data={tree} renderItem={renderNode} />Label, description & error
Built-in via Field, like the rest of the family:
Code
<TreeSelectlabel="File"description="Choose a file from the tree"data={tree}/>API
Prop
Type
Branches are organizational only — they render as non-interactive rows. To pick a category itself, model it as a leaf (give it no children) or use TreeMultiSelect, which selects whole branches.
Accessibility
- The tree is
role="tree"(single-select, so noaria-multiselectable); each leaf is arole="treeitem"carryingaria-selectedandaria-level, and each branch arole="treeitem"witharia-expanded. The selected leaf is marked with a trailing check. ↑/↓move between selectable leaves (branches are skipped),Home/Endjump to the first / last leaf, andSpace/Enterpick the focused leaf and close. There is no expand / collapse — the tree is always fully shown.- The trigger is a
role="combobox"that opens the Popover; it traps focus and dismisses on outside-click /Escape. Label / description / error are associated via the built-inField. - When
searchable, the popover opens with focus in the search field, and↓moves focus from the field into the first leaf.