TreeMultiSelect
Pick several values from hierarchical data — an always-expanded checkbox tree in a Popover, with cascading parent selection.
TreeMultiSelect lets the user pick several values from hierarchical data. It pairs a pills trigger (like MultiSelect) with an always-expanded checkbox tree in a Popover: top-level rows sit flush, their descendants are connected by indent guide lines, and checking a folder cascades to everything inside it. A search field at the top of the popover filters the tree.
import { TreeMultiSelect } from '@42-central/ui-react/tree-multi-select';
<TreeMultiSelect
data={[
{ value: 'srcs', label: 'srcs', children: [
{ value: 'main.c', label: 'main.c' },
]},
{ value: 'Makefile', label: 'Makefile' },
]}
placeholder="Pick files"
/>For a single-value tree picker, reach for TreeSelect.
Playground
Component
Dir
Presets
Props
Code
<Component
placeholder="Pick files"
clearSectionMode="replace"
searchPlaceholder="Search…"
size="md"
/>Data
data is a tree of nodes — each { value, label, disabled?, children? }. A node with children is a branch: checking it toggles every leaf beneath it, and it reads as indeterminate when only some descendants are selected. A node without children is a leaf. 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' },
]},
];Each node also accepts an optional testId, for a stable data-testid on that node's own row
regardless of its label — see the Testing guide. It defaults to
`item-${value}` when omitted.
Controlled
value is the array of selected leaf values; onChange fires with the full selection on every toggle or pill removal. Branch state is derived, so the value never contains branch nodes.
'use client';
import { useState } from 'react';
import { TreeMultiSelect } from '@42-central/ui-react/tree-multi-select';
export function FilePicker({ tree }) {
const [value, setValue] = useState<string[]>([]);
return <TreeMultiSelect data={tree} value={value} onChange={setValue} />;
}Selection
Checking a folder selects everything inside it; partially-filled folders show an indeterminate state. Remove a value from its pill, or clear them all with clearable.
Code
<TreeMultiSelect
clearable
defaultValue={['lexer.c', 'ft_cd.c']}
data={tree}
/>Max lines
maxLines caps the trigger's pills at N rows; any beyond collapse into a static +N counter. The hidden values stay selected — open the tree to deselect them.
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 { checked, indeterminate, leaf, disabled, depth } state. TreeMultiSelect keeps the row — treeitem role, indent rails, roving focus, click, and the cascade — and the callback fills everything after the rails, including the checkbox (the default one is part of the content it replaces). Read state.checked / state.indeterminate to draw your own tri-state box. Define the function in a "use client" module so it survives the server → client boundary.
'use client';
import { Check, Minus } from 'lucide-react';
const renderNode = (node, { checked, indeterminate }) => (
<span className="flex flex-1 items-center gap-2">
<span className="flex size-4 items-center justify-center rounded-xs border border-(--c-solid) bg-(--c-solid) text-(--c-on-solid)">
{checked && <Check className="size-3" />}
{indeterminate && <Minus className="size-3" />}
</span>
<span className="flex-1 truncate text-start">{node.label}</span>
</span>
);
<TreeMultiSelect 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-central/ui-react/tree-multi-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>
);
<TreeMultiSelect data={tree} renderItem={renderNode} />Label, description & error
Built-in via Field, like the rest of the family:
Code
<TreeMultiSelect
label="Files"
description="Choose files from the tree"
data={tree}
/>API
Prop
Type
value holds leaf values only — a branch's checked / indeterminate state is derived from its descendants, so each selected leaf shows as its own pill in the trigger.Accessibility
- The tree is
role="tree"witharia-multiselectable; each row is arole="treeitem"carryingaria-checked(true/false/mixed) andaria-level. The checkbox is presentational — selection state lives on the row. ↑/↓move between rows,Home/Endjump to the first / last, andSpace/Entertoggle the focused node (cascading to its children). 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 tree row.
Changelog
- #331
A
translationsprop for the built-in trigger, clear, remove, search, increment/decrement and upload labels - #316
Option checkboxes render exactly like a standalone
Checkbox, including its sizes (mdgrows from 16px to 18px) - #325
The trigger is named by the Field label, announces the description or error, and its
aria-controlspoints at the tree - #326
Tree rows and Notification's action, close and collapse buttons show the kit's opaque 2px focus ring instead of a faint translucent one
- #71
valueandonChangenarrow to the literal values ofas constdata, like Select
- #37
Borders and dividers on floating panels match the panel's own surface
The picker family:
Select,MultiSelect,Autocomplete,Combobox,TreeSelectandTreeMultiSelectrenderItemacross the picker family, with a typed item payloadclearSectionModepicks whether the clear button replaces the chevron or sits beside itPickers open on a tap anywhere in the field, and their clear button is keyboard-reachable