Tree Select

A tree in a popover with real tri-state parents. Checking a branch checks its subtree, the value holds only leaves, and searching keeps the ancestors of every match.

src/components/ui/tree-select.tsx

About

From the component's own source, where it was written.

A tree in a popover, with checkboxes and real tri-state parents.

**The tri-state is the whole problem.** A parent whose children are partly chosen is neither checked nor unchecked, and rendering it as unchecked is a lie people act on — they tick it, and silently replace a careful selection with everything. The indeterminate state is set through the DOM property, because indeterminate is not an HTML attribute and cannot be expressed in JSX; aria-checked="mixed" carries the same fact to assistive technology.

**Checking a branch checks its subtree, and value holds only the leaves.** Storing branch ids as well means every consumer has to know whether a parent in the list implies its children, and two consumers will answer differently. Leaves are unambiguous, and parent state is derived on render.

Searching filters to matching nodes **and keeps their ancestors**, because a result with its path cut off is unplaceable — you cannot tell which "General" you found.

Install

npx astralyx-ui add tree-select

Usage

import { TreeSelect } from '@/components/ui/tree-select'

<TreeSelect nodes={scopes} value={selected} onChange={setSelected} />

Composer

Change the props and the source below follows.

Props
tsx
<TreeSelect
  nodes={scopes}
  value={selected}
  onChange={setSelected}
  searchable={true}
/>

Examples

Token scopes

API

Generated from the source, so it cannot fall behind the code. One table per exported component, then hooks, functions and types.

TreeSelect

Extends Omit<ComponentProps<'div'>, 'onChange' | 'defaultValue'>.

PropTypeDefaultDescription
nodesTreeSelectNode[]requiredThe value holds leaves only — a parent in the list would leave every consumer guessing whether it implies its children.
valuestring[]—The value holds leaves only — a parent in the list would leave every consumer guessing whether it implies its children.
defaultValuestring[][]—
onChange(value: string[]) => void——
placeholderstring'Select…'—
searchablebooleantrue—
searchPlaceholderstring'Search'—
defaultExpandedstring[][]Values of branches open on first render.
size'sm' | 'md' | 'lg''md'—
summary(values: string[]) => ReactNode—Summarises the selection on the trigger.
emptyLabelstring'Nothing matches.'—
disabledboolean——
invalidboolean——
labelstring——

Types

TreeSelectNode

A tree in a popover, with checkboxes and real tri-state parents.

FieldTypeDescription
valuestring—
labelReactNode—
children?TreeSelectNode[]—
disabled?boolean—

Behaviour

TopicDetailDescription
tri-statethe whole problemA partly-chosen parent is neither checked nor unchecked. Rendering it as unchecked is a lie people act on: they tick it and silently replace a careful selection with everything.
indeterminatea propertyNot an HTML attribute, so it cannot be expressed in JSX — it is set through a ref callback, with aria-checked="mixed" carrying the same fact.
searchkeeps ancestorsA result with its path cut off is unplaceable: you cannot tell which "General" you found. Matching branches are force-opened.