Components Navigation
TreeView
StableDisplays and navigates hierarchical data with expandable branches.
Use TreeView for nested structures people need to browse and pick from: teams in an organisation, folders, categories. It follows the WAI-ARIA tree pattern, including typeahead and expanding all siblings with *. Data is passed as items, and expansion and selection can be controlled.
Overview
- Binds
- Sales
- Support
- Legal
- Marketing
import { TreeView } from '@bindshq/stance-react';
import type { TreeItem } from '@bindshq/stance-react';
const teams: TreeItem[] = [
{
id: 'binds',
label: 'Binds',
children: [
{
id: 'sales',
label: 'Sales',
meta: '48',
children: [
{ id: 'emea', label: 'EMEA', meta: '21' },
{ id: 'apac', label: 'APAC', meta: '27' },
],
},
{
id: 'support',
label: 'Support',
meta: '16',
children: [
{ id: 'tier1', label: 'Tier 1', meta: '10' },
{ id: 'tier2', label: 'Tier 2', meta: '6' },
],
},
{ id: 'legal', label: 'Legal', meta: '4', disabled: true },
{ id: 'marketing', label: 'Marketing', meta: '9' },
],
},
];
export function Basic() {
return (
<TreeView
aria-label="Teams"
items={teams}
defaultExpanded={['binds']}
defaultSelected={['sales']}
style={{ width: 280 }}
/>
);
}Light, dark and high contrast
Every Stance component is designed and tested in all three appearance modes.
- Binds
- Sales
- Support
- Legal
- Marketing
- Binds
- Sales
- Support
- Legal
- Marketing
- Binds
- Sales
- Support
- Legal
- Marketing
Import
import { TreeView, type TreeItem } from '@bindshq/stance-react';Examples
Multiple selection
Space toggles, Ctrl/⌘+A selects everything, Shift+arrows extends.
- Binds
- Sales
- EMEA
- APAC
- Support
- Legal
- Marketing
1 selected
import { useState } from 'react';
import { TreeView } from '@bindshq/stance-react';
import type { TreeItem } from '@bindshq/stance-react';
const teams: TreeItem[] = [
{
id: 'binds',
label: 'Binds',
children: [
{
id: 'sales',
label: 'Sales',
meta: '48',
children: [
{ id: 'emea', label: 'EMEA', meta: '21' },
{ id: 'apac', label: 'APAC', meta: '27' },
],
},
{
id: 'support',
label: 'Support',
meta: '16',
children: [
{ id: 'tier1', label: 'Tier 1', meta: '10' },
{ id: 'tier2', label: 'Tier 2', meta: '6' },
],
},
{ id: 'legal', label: 'Legal', meta: '4', disabled: true },
{ id: 'marketing', label: 'Marketing', meta: '9' },
],
},
];
export function MultipleSelection() {
const [selected, setSelected] = useState<string[]>(['emea']);
return (
<div style={{ display: 'grid', gap: 12, width: 280 }}>
<TreeView
aria-label="Assign to teams"
items={teams}
selectionMode="multiple"
selected={selected}
onSelectedChange={setSelected}
defaultExpanded={['binds', 'sales']}
/>
<p style={{ margin: 0, fontSize: 13 }}>{selected.length} selected</p>
</div>
);
}- Binds
- Sales
- Support
- Legal
- Marketing
import { Icon, TreeView } from '@bindshq/stance-react';
import type { TreeItem } from '@bindshq/stance-react';
const teams: TreeItem[] = [
{
id: 'binds',
label: 'Binds',
children: [
{
id: 'sales',
label: 'Sales',
meta: '48',
children: [
{ id: 'emea', label: 'EMEA', meta: '21' },
{ id: 'apac', label: 'APAC', meta: '27' },
],
},
{
id: 'support',
label: 'Support',
meta: '16',
children: [
{ id: 'tier1', label: 'Tier 1', meta: '10' },
{ id: 'tier2', label: 'Tier 2', meta: '6' },
],
},
{ id: 'legal', label: 'Legal', meta: '4', disabled: true },
{ id: 'marketing', label: 'Marketing', meta: '9' },
],
},
];
export function CustomIcons() {
return (
<TreeView
aria-label="Teams"
items={teams}
defaultExpanded={['binds']}
selectionMode="none"
renderIcon={(item, s) => (
<Icon name={s.level === 1 ? 'building' : s.hasChildren ? 'users' : 'user'} size={16} />
)}
style={{ width: 280 }}
/>
);
}- Binds
- Sales
- Support
- Legal
- Marketing
import { useState } from 'react';
import { TreeView } from '@bindshq/stance-react';
import type { TreeItem } from '@bindshq/stance-react';
const teams: TreeItem[] = [
{
id: 'binds',
label: 'Binds',
children: [
{
id: 'sales',
label: 'Sales',
meta: '48',
children: [
{ id: 'emea', label: 'EMEA', meta: '21' },
{ id: 'apac', label: 'APAC', meta: '27' },
],
},
{
id: 'support',
label: 'Support',
meta: '16',
children: [
{ id: 'tier1', label: 'Tier 1', meta: '10' },
{ id: 'tier2', label: 'Tier 2', meta: '6' },
],
},
{ id: 'legal', label: 'Legal', meta: '4', disabled: true },
{ id: 'marketing', label: 'Marketing', meta: '9' },
],
},
];
export function Controlled() {
const [expanded, setExpanded] = useState<string[]>(['binds']);
return (
<div style={{ display: 'grid', gap: 12, width: 280 }}>
<TreeView
aria-label="Teams"
items={teams}
expanded={expanded}
onExpandedChange={setExpanded}
/>
<div style={{ display: 'flex', gap: 8 }}>
<button type="button" onClick={() => setExpanded(['binds', 'sales', 'support'])}>
Expand all
</button>
<button type="button" onClick={() => setExpanded([])}>
Collapse all
</button>
</div>
</div>
);
}API reference
| Prop | Type | Default | Description |
|---|---|---|---|
items* required | TreeItem[] | None | { id, label, children?, icon?, disabled?, meta? }. |
aria-label | string | None | Name of the tree (or use aria-labelledby). |
selectionMode | 'none' | 'single' | 'multiple' | 'single' | How many items can be selected. |
selected / defaultSelected | string[] | None | Selected ids. |
onSelectedChange | (ids: string[]) => void | None | Called when selection changes. |
expanded / defaultExpanded | string[] | None | Expanded ids. |
onExpandedChange | (ids: string[]) => void | None | Called when branches open or close. |
onAction | (item: TreeItem) => void | None | Called on Enter or click, e.g. to open an item. |
renderIcon | (item, state) => ReactNode | None | Icon per item using { expanded, selected, hasChildren, level }. |
size | 'sm' | 'md' | 'md' | Row height. |
Accessibility
- tree, treeitem and group roles with aria-level, aria-setsize, aria-posinset, aria-expanded and aria-selected.
- One item is in the Tab order (roving tabindex); disabled items are skipped by arrow keys and announced as disabled.
- aria-multiselectable is set in multiple selection mode.
- Selection is shown with a tint and weight change plus an outline under forced colours.
Keyboard
| Keys | Action |
|---|---|
| ↑ ↓ | Previous / next visible item. |
| → | Expands a closed branch, or moves to its first child. |
| ← | Collapses an open branch, or moves to the parent. |
| Home End | First / last visible item. |
| Enter | Selects and activates the item. |
| Space | Selects (toggles in multiple mode). |
| * | Expands all siblings of the focused item. |
| a to z | Moves to the next item starting with the typed characters. |
| Ctrl/⌘ + A | Selects all (multiple mode). |
Usage guidelines
Do
- Keep labels short and unique among siblings.
- Show counts with meta rather than in the label.
Don’t
- Use a tree for fewer than two levels; use a List.
- Nest more than four or five levels deep.