Skip to content

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.

tsx
import { TreeSelect } from 'neba';

<TreeSelect label="Category" items={categories} value={value} onValueChange={setValue} />;

Props

PropTypeDefaultDescription
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
elevationshared0 | 1 | 2 | 30Drop shadow depth. 0 means no shadow at all
itemsTreeSelectItem[]The tree, as nested items
valueTreeViewValue | TreeViewValue[] | nullThe chosen value, or values when multiple. Use with onValueChange for a controlled one
defaultValueTreeViewValue | TreeViewValue[] | nullThe initial value, for an uncontrolled one
onValueChange(value: TreeViewValue[]) => voidCalled when the selection changes: an array even in single mode
multiplebooleanfalseWhether more than one node may be held
selectableBranchesbooleanfalseWhether 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
searchablebooleanfalseAdds 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
clearLabelstringThe clear button's accessible name. Defaults to the locale's wording
searchPlaceholderstringThe search field's placeholder
expandedTreeViewValue[]Which branches are open. Use with onExpandedChange to control them
defaultExpandedTreeViewValue[]Which branches start open
onExpandedChange(expanded: TreeViewValue[]) => voidCalled when a branch opens or closes
format(chosen: TreeSelectItem[]) => ReactNodeHow the trigger writes what is held. The labels, comma-joined, by default
closeOnSelectboolean!multipleCloses the popup as soon as a node is chosen
namestringIdentifies the field when a form is submitted: one hidden input per value
openbooleanWhether the popup is open. Use with `onOpenChange` for a controlled popup
defaultOpenbooleanWhether the popup starts open
onOpenChange(open: boolean) => voidCalled when the popup opens or closes
placeholderReactNodeShown in the trigger while nothing is chosen
clearablebooleanfalseOffers the × that empties the control
localestringBCP 47 tag deciding the strings it draws on its own behalf
classNamesNebaSlots<'popup' | 'tree' | 'item' | 'empty'>Class names for the parts behind the root. The root itself is className, so there is no root key
labelReactNodeThe label, wired to the control by Base UI's Field
descriptionReactNodeHelper text
errorReactNodeError message. Its presence turns the control invalid and re-points the colour family at danger
invalidboolean!!errorForces the invalid state without a message, for when a form library owns validity
readOnlybooleanfalseShown but not changeable. Keeps its colour and edge, drains the saturation
disabledbooleanfalseUnavailable. Drops the colour family for neutral grey

The items

PropTypeDefaultDescription
valuestring | numberWhat is stored when this node is chosen. Unique across the whole tree, not just among siblings
labelReactNodeWhat the row draws
searchLabelstringWhat a search matches against. Falls back to label when it is a string
startIconReactNodeA glyph before the label
selectablebooleanWhether this node may itself be chosen. true for a leaf; a node with children follows selectableBranches
disabledbooleanCannot be chosen
childrenTreeSelectItem[]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 areUse
A flat listSelect
A flat list you type intoCombobox
A hierarchy, and you are choosing from itTreeSelect
A hierarchy you are showing rather than choosing fromTreeView

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.

tsx
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" of role="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-disabled and keeps its place, so it stays on the arrow-key path.
  • The popup is portalled to the end of <body>, with neba-portal on the positioner.

Released under the MIT License