Form
Tree Select
Select from a hierarchy with parent/child checkboxes — leaf-based, searchable, in React (@bpdm/ui) and Angular (@bpdm/ng).
The Tree Select picks options from a hierarchy. Toggling a parent selects
all its children; selection is leaf-based — value is the array of selected
leaf values. Nodes are data-driven via options (each node can have children).
Usage
ReactVue
import { TreeSelect } from '@bpdm/ui/tree-select';
const TREE = [
{
value: 'frontend',
label: 'Frontend',
children: [
{ value: 'react', label: 'React' },
{ value: 'angular', label: 'Angular' },
{ value: 'vue', label: 'Vue' },
],
},
{
value: 'backend',
label: 'Backend',
children: [
{ value: 'node', label: 'Node.js' },
{ value: 'go', label: 'Go' },
{ value: 'rust', label: 'Rust' },
],
},
];
export function TreeSelectUsage() {
return <TreeSelect options={TREE} defaultValue={['react', 'vue']} placeholder="Select technologies" />;
}import { Component } from '@angular/core';
import { BpdmTreeSelect } from '@bpdm/ng';
@Component({
selector: 'tree-select-usage',
imports: [BpdmTreeSelect],
templateUrl: './tree-select-usage.html',
})
export class TreeSelectUsage {
readonly tree = [
{ value: 'frontend', label: 'Frontend', children: [
{ value: 'react', label: 'React' },
{ value: 'angular', label: 'Angular' },
{ value: 'vue', label: 'Vue' },
] },
{ value: 'backend', label: 'Backend', children: [
{ value: 'node', label: 'Node.js' },
{ value: 'go', label: 'Go' },
{ value: 'rust', label: 'Rust' },
] },
];
}<bpdm-tree-select [options]="tree" [defaultValue]="['react', 'vue']" placeholder="Select technologies" />Searchable
Set searchable to filter the tree — matches stay visible with their ancestors
expanded.
Search technologies
import { TreeSelect } from '@bpdm/ui/tree-select';
const TREE = [
{ value: 'frontend', label: 'Frontend', children: [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
] },
{ value: 'backend', label: 'Backend', children: [
{ value: 'node', label: 'Node.js' },
{ value: 'go', label: 'Go' },
] },
];
export function SearchableTree() {
return <TreeSelect options={TREE} searchable placeholder="Search technologies" />;
}import { Component } from '@angular/core';
import { BpdmTreeSelect } from '@bpdm/ng';
@Component({
selector: 'app-searchable-tree',
standalone: true,
imports: [BpdmTreeSelect],
template: `<bpdm-tree-select [options]="tree" searchable placeholder="Search technologies" />`,
})
export class SearchableTreeComponent {
readonly tree = [
{ value: 'frontend', label: 'Frontend', children: [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
] },
{ value: 'backend', label: 'Backend', children: [
{ value: 'node', label: 'Node.js' },
{ value: 'go', label: 'Go' },
] },
];
}Count display
Like Multi-select, set maxDisplay={0} to show a compact "N selected" count
instead of chips.
4 selected
import { TreeSelect } from '@bpdm/ui/tree-select';
const TREE = [
{ value: 'frontend', label: 'Frontend', children: [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
] },
{ value: 'backend', label: 'Backend', children: [
{ value: 'node', label: 'Node.js' },
{ value: 'go', label: 'Go' },
] },
];
export function CountDisplay() {
return <TreeSelect options={TREE} maxDisplay={0} defaultValue={['react', 'vue', 'node']} />;
}import { Component } from '@angular/core';
import { BpdmTreeSelect } from '@bpdm/ng';
@Component({
selector: 'app-count-display',
standalone: true,
imports: [BpdmTreeSelect],
template: `<bpdm-tree-select [options]="tree" [maxDisplay]="0" [defaultValue]="['react', 'vue', 'node']" />`,
})
export class CountDisplayComponent {
readonly tree = [
{ value: 'frontend', label: 'Frontend', children: [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
] },
{ value: 'backend', label: 'Backend', children: [
{ value: 'node', label: 'Node.js' },
{ value: 'go', label: 'Go' },
] },
];
}States
Default
Invalid
Disabled
import { TreeSelect } from '@bpdm/ui/tree-select';
const TREE = [
{ value: 'frontend', label: 'Frontend', children: [{ value: 'react', label: 'React' }] },
{ value: 'backend', label: 'Backend', children: [{ value: 'node', label: 'Node.js' }] },
];
export function States() {
return (
<div className="flex flex-col gap-3">
<TreeSelect options={TREE} placeholder="Default" />
<TreeSelect options={TREE} aria-invalid placeholder="Invalid" />
<TreeSelect options={TREE} disabled placeholder="Disabled" />
</div>
);
}import { Component } from '@angular/core';
import { BpdmTreeSelect } from '@bpdm/ng';
@Component({
selector: 'app-states',
standalone: true,
imports: [BpdmTreeSelect],
template: `
<div class="flex flex-col gap-3">
<bpdm-tree-select [options]="tree" placeholder="Default" />
<bpdm-tree-select [options]="tree" aria-invalid="true" placeholder="Invalid" />
<bpdm-tree-select [options]="tree" disabled placeholder="Disabled" />
</div>
`,
})
export class StatesComponent {
readonly tree = [
{ value: 'frontend', label: 'Frontend', children: [{ value: 'react', label: 'React' }] },
{ value: 'backend', label: 'Backend', children: [{ value: 'node', label: 'Node.js' }] },
];
}API
| Prop | Type | Default | Description |
|---|---|---|---|
options | TreeNode[] ({ value, label, disabled?, children? }) | — | Required. The hierarchy. |
value / defaultValue | string[] | — | Selected leaf values (controlled / uncontrolled). |
onValueChange | (value: string[]) => void | — | React — fires on change. Angular uses [(value)]. |
placeholder | string | Select… | Text when nothing is selected. |
maxDisplay | number | 3 | Chips before "+N"; 0 → "N selected". |
selectAll | boolean | true | Show a "Select all" row. |
searchable | boolean | false | Filter the tree. |
searchPlaceholder | string | Search… | Filter placeholder. |
emptyText | string | No results. | Shown when nothing matches. |
size | sm | md | lg | md | Trigger height / text size. |
disabled | boolean | false | Disable the control. |
aria-invalid | boolean | — | Error state. |
id | string | — | Trigger id (for <label> association / testing). |
aria-label / aria-describedby | string | — | Accessible name / description for the trigger + tree. |
messages | { expand, collapse, selectAll, clearAll, selected(count) } | English | Override the built-in labels + count text for i18n. |
Accessibility
- Built on the WAI-ARIA tree pattern: the panel is
role="tree"(aria-multiselectable), each node is arole="treeitem"carryingaria-level,aria-expanded(branches), and its chosen state as botharia-selectedandaria-checked(true/false/mixed— a parent ismixedwhen only some descendants are chosen); child nodes sit in a nestedrole="group". The trigger isrole="combobox"witharia-haspopup="tree"andaria-controls. Both state attributes are set so more screen readers surface the state — some readaria-selected, othersaria-checked. - The caret and the checkbox glyph are decorative (
aria-hidden) — state is conveyed byaria-expanded/aria-checked, never a glyph alone. Select all is a tri-staterole="checkbox". - Fully operable by keyboard. The tree is a single tab stop and tracks the active node with
aria-activedescendant: Up / Down move between visible treeitems, Right expands a collapsed branch then steps into it, Left collapses an expanded branch or moves to the parent (both flip underdir="rtl"), Home / End jump to the first / last visible node, Enter / Space toggle selection, type-ahead jumps to the next matching label, and Esc closes. Give the field a name via<label>(id) oraria-label, and setaria-invalidon error.
Internationalization
- All built-in copy is translatable:
placeholder/searchPlaceholder/emptyTextare props, and themessagesprop overrides the branchexpand/collapselabels,selectAll,clearAll, and theselected(count)count text. Node labels are your data. - RTL-safe: node indentation uses
padding-inline-start, the caret mirrors underdir="rtl", and labels use logical alignment (text-start), so the whole tree flips correctly right-to-left. See the Internationalization guide.