TreeSelect
A value chosen from a tree rather than from a list. For a category, a folder, a region or an org-chart node: the things a flat list flattens away.
import { TreeSelect } from 'neba';
<TreeSelect label="Category" items={categories} value={value} onValueChange={setValue} />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the surface. The same shell as a TextField and a Select |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Height and type scale |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only: never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| items | TreeSelectItem[] | — | The tree, as nested items |
| value | TreeViewValue | TreeViewValue[] | null | — | The chosen value, or values when multiple. Use with onValueChange for a controlled one |
| defaultValue | TreeViewValue | TreeViewValue[] | null | — | The initial value, for an uncontrolled one |
| onValueChange | (value: TreeViewValue[]) => void | — | Called when the selection changes: an array even in single mode |
| multiple | boolean | false | Whether more than one node may be held |
| selectableBranches | boolean | false | Whether a node with children may itself be chosen. Off by default because in most of these trees the branches are the taxonomy and the leaves are the answers: a Europe choosable alongside France is a data model nobody meant |
| searchable | boolean | false | Adds a field above the tree that filters it. A match keeps its ancestors and every kept branch is opened: a match without them is a list, and a folded one was not shown |
| clearLabel | string | — | The clear button's accessible name. Defaults to the locale's wording |
| searchPlaceholder | string | — | The search field's placeholder |
| expanded | TreeViewValue[] | — | Which branches are open. Use with onExpandedChange to control them |
| defaultExpanded | TreeViewValue[] | — | Which branches start open |
| onExpandedChange | (expanded: TreeViewValue[]) => void | — | Called when a branch opens or closes |
| format | (chosen: TreeSelectItem[]) => ReactNode | — | How the trigger writes what is held. The labels, comma-joined, by default |
| closeOnSelect | boolean | !multiple | Closes the popup as soon as a node is chosen |
| name | string | — | Identifies the field when a form is submitted: one hidden input per value |
| open | boolean | — | Whether the popup is open. Use with `onOpenChange` for a controlled popup |
| defaultOpen | boolean | — | Whether the popup starts open |
| onOpenChange | (open: boolean) => void | — | Called when the popup opens or closes |
| placeholder | ReactNode | — | Shown in the trigger while nothing is chosen |
| clearable | boolean | false | Offers the × that empties the control |
| locale | string | — | BCP 47 tag deciding the strings it draws on its own behalf |
| classNames | NebaSlots<'popup' | 'tree' | 'item' | 'empty'> | — | Class names for the parts behind the root. The root itself is className, so there is no root key |
| label | ReactNode | — | The label, wired to the control by Base UI's Field |
| description | ReactNode | — | Helper text |
| error | ReactNode | — | Error message. Its presence turns the control invalid and re-points the colour family at danger |
| invalid | boolean | !!error | Forces the invalid state without a message, for when a form library owns validity |
| readOnly | boolean | false | Shown but not changeable. Keeps its colour and edge, drains the saturation |
| disabled | boolean | false | Unavailable. Drops the colour family for neutral grey |
The items
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | number | — | What is stored when this node is chosen. Unique across the whole tree, not just among siblings |
| label | ReactNode | — | What the row draws |
| searchLabel | string | — | What a search matches against. Falls back to label when it is a string |
| startIcon | ReactNode | — | A glyph before the label |
| selectable | boolean | — | Whether this node may itself be chosen. true for a leaf; a node with children follows selectableBranches |
| disabled | boolean | — | Cannot be chosen |
| children | TreeSelectItem[] | — | The nodes under this one |
value must be unique across the whole tree, not just among siblings: it is what the component looks a node up by.
Which one to use
| The options are | Use |
|---|---|
| A flat list | Select |
| A flat list you type into | Combobox |
| A hierarchy, and you are choosing from it | TreeSelect |
| A hierarchy you are showing rather than choosing from | TreeView |
Examples
selectableBranches
Off by default, and that default carries weight: in most of these trees the branches are the taxonomy and the leaves are the answers. A "Europe" that can be chosen alongside "France" is usually a data model nobody meant.
A branch still expands and collapses when it cannot be chosen. An item's own selectable overrides the setting either way, which is how you get one choosable branch in a tree of headings, or one heading in a tree of choosable nodes.
multiple
Holds any number, and the trigger writes them comma-joined unless format says otherwise. closeOnSelect follows it: a single-value TreeSelect closes on the first pick, a multiple one stays open.
searchable
Adds a field above the tree that filters it.
A match keeps its ancestors, and every branch the filter kept is opened. Both halves matter: a tree filtered to bare matches is a list, and a list of leaves is exactly what a tree was chosen over. "Seoul" with nothing above it does not say which taxonomy it came from, and a match folded inside a closed parent is a match the reader was not shown.
searchLabel is what a node is matched against when its label is a node rather than a string.
format
How the trigger writes what is held.
format={(chosen) => (chosen.length === 1 ? chosen[0].label : `${chosen.length} categories`)}name
Submits with a form as one hidden input per value, so multiple arrives as a repeated field the way a <select multiple> does.
Accessibility
- The popup holds a
role="tree"ofrole="treeitem"rows, with arrow-key navigation and a single tab stop, from TreeView. - A branch's accessible name includes its subtree, because the row's element contains its children. Query and test by the row's own text.
- A node that cannot be chosen carries
aria-disabledand keeps its place, so it stays on the arrow-key path. - The popup is portalled to the end of
<body>, withneba-portalon the positioner.