# TreeView

> Displays and navigates hierarchical data with expandable branches.

Category: Navigation · Status: stable · Package: @bindshq/stance-react@0.1.1 · Docs: https://stanceui.com/components/tree-view

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.

## Import

```tsx
import { TreeView, type TreeItem } from '@bindshq/stance-react';
```

Styles are imported once per app: `@bindshq/stance-tokens/css` and `@bindshq/stance-react/styles.css`.

## Examples

### Basic

```tsx
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 }}
    />
  );
}
```

### Multiple selection

Space toggles, Ctrl/⌘+A selects everything, Shift+arrows extends.

```tsx
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>
  );
}
```

### Custom icons

```tsx
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 }}
    />
  );
}
```

### Controlled expansion

```tsx
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

| Component | Prop | Type | Default | Description |
| --- | --- | --- | --- | --- |
| TreeView | `items` (required) | `TreeItem[]` | None | { id, label, children?, icon?, disabled?, meta? }. |
| TreeView | `aria-label` | `string` | None | Name of the tree (or use aria-labelledby). |
| TreeView | `selectionMode` | `'none' \| 'single' \| 'multiple'` | `'single'` | How many items can be selected. |
| TreeView | `selected / defaultSelected` | `string[]` | None | Selected ids. |
| TreeView | `onSelectedChange` | `(ids: string[]) => void` | None | Called when selection changes. |
| TreeView | `expanded / defaultExpanded` | `string[]` | None | Expanded ids. |
| TreeView | `onExpandedChange` | `(ids: string[]) => void` | None | Called when branches open or close. |
| TreeView | `onAction` | `(item: TreeItem) => void` | None | Called on Enter or click, e.g. to open an item. |
| TreeView | `renderIcon` | `(item, state) => ReactNode` | None | Icon per item using { expanded, selected, hasChildren, level }. |
| TreeView | `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). |

## 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.

## Related

- https://stanceui.com/components/file-tree
- https://stanceui.com/components/accordion
- https://stanceui.com/components/list
