# Tree

**When to use:** A hierarchical list the user can expand, collapse and pick from: categories, nested locations, folder structures, org charts. For a flat list of thousands of rows use [VirtualList](VirtualList.md); for picking several unrelated values use [MultiSelect](MultiSelect.md).

**Keywords:** hierarchy, directory, expand collapse, parent child, drilldown, navigation sidebar, outline, subfolder

**Import:** `import { Tree, flattenTree } from '@ahrowe/ui'`  
**Types:** `import type { TreeProps, TreeNode, TreeValue, FlatTreeNode } from '@ahrowe/ui'`

```tsx
import { useState } from 'react';
import { Tree } from '@ahrowe/ui';
import type { TreeNode, TreeValue } from '@ahrowe/ui';

const nodes: TreeNode[] = [
  {
    key: 'hall-1',
    label: 'Hall 1',
    children: [
      { key: 'bay-a', label: 'Bay A' },
      { key: 'bay-b', label: 'Bay B' },
    ],
  },
  { key: 'archive', label: 'Archive', disabled: true },
];

// Controlled selection
const [selected, setSelected] = useState<TreeValue | null>(null);
<Tree nodes={nodes} selectedKey={selected} onSelect={setSelected} aria-label="Locations" />

// Uncontrolled, with some branches open to begin with
<Tree nodes={nodes} defaultExpandedKeys={['hall-1']} defaultSelectedKey="bay-a" />

// Controlled expansion, e.g. to persist it
<Tree nodes={nodes} expandedKeys={open} onExpandedChange={setOpen} />

// Clicking a branch selects it without opening it
<Tree nodes={nodes} expandOnSelect={false} />

// Icons per node, and a wider indent
import { faWarehouse } from '@fortawesome/free-solid-svg-icons';
<Tree nodes={nodes.map((n) => ({ ...n, icon: faWarehouse }))} indent={40} />
```

**Branch or leaf:** a node is a branch when it has a `children` array, even an empty one. `children: []` renders a toggle with nothing behind it yet, which is what you want while a branch's contents are still loading; omit `children` entirely for a true leaf.

**Selection is single.** `onSelect` fires with the key and the node itself, so you do not have to look it up again. There is no multi-select or checkbox mode.

**Key props:**

| Prop | Type | Description |
|------|------|-------------|
| `nodes` | `TreeNode[]` | The tree: `{ key, label, icon?, children?, disabled? }` |
| `expandedKeys` | `TreeValue[]` | Controlled open branches (omit for uncontrolled) |
| `defaultExpandedKeys` | `TreeValue[]` | Branches open initially when uncontrolled (default `[]`) |
| `onExpandedChange` | `(keys: TreeValue[]) => void` | Fires with the full new set of open branches |
| `selectedKey` | `TreeValue \| null` | Controlled selection (omit for uncontrolled) |
| `defaultSelectedKey` | `TreeValue` | Selection when uncontrolled |
| `onSelect` | `(key: TreeValue, node: TreeNode) => void` | Fires with the picked key and node |
| `expandOnSelect` | `boolean` | Clicking a branch's row also toggles it (default `true`) |
| `indent` | `number` | Pixels per level (default `20`) |
| `toggleIcon` | `IconDefinition \| ReactElement` | Toggle icon, rotated 90° when open (default a right chevron) |
| `height` | `number \| string` | Windows the rows through `VirtualList` at this viewport height |
| `estimatedRowHeight` | `number` | Row height for the windowed renderer (default `32`) |
| `labels.empty` | `string` | Shown when `nodes` is empty (default `'Nothing here'`) |
| `disabled` | `boolean` | Dims the tree and blocks interaction |
| `aria-label` | `string` | Accessible name for the tree (default `'Tree'`) |

**Keyboard:** `↑`/`↓` move between visible rows. `→` opens a closed branch, then steps into its first child. `←` closes an open branch, then steps out to its parent. `Home`/`End` jump to the ends. `Enter` or `Space` selects.

**Accessibility:** `role="tree"` containing `role="treeitem"` rows. Because the rows render flat rather than nested, each carries `aria-level`, `aria-posinset` and `aria-setsize`; branches also carry `aria-expanded`. Focus uses a roving tabindex, so one row at a time is reachable by Tab, and it is the selected one (or the first, before anything is selected). The toggle is `aria-hidden`, since `aria-expanded` on the row already reports the state.

**Why flat:** the rows are a flattened list, not nested `<div>`s. Keyboard movement becomes a step along one array instead of a tree walk, and a node's accessible name stays its own label rather than absorbing its descendants'.

**Very large trees:** set `height`. `Tree` then windows its rows through [VirtualList](VirtualList.md), so only the ones in view are in the DOM, and it keeps everything else: the ARIA, the expansion, the selection and the keyboard.

```tsx
<Tree nodes={nodes} height={300} expandedKeys={open} onExpandedChange={setOpen} />
```

Two things change in that mode. Rows must be a uniform height, so size them with `--tree-node-height` rather than per row. And the tab stop moves from the selected row to the tree itself, because a roving tabindex on a row that is scrolled out of the window would leave the tree with no tab stop at all; tabbing in still lands on the selected row, which is scrolled into view first.

`flattenTree` is exported separately if you want the rows without the component: it returns `FlatTreeNode`s carrying `node`, `level`, `parentKey`, `isBranch`, `isExpanded`, `setSize` and `posInSet`, ready for `VirtualList`'s `items` and `getItemDepth` (which also feeds its `treeReorder` drag-and-drop). You then write the row markup, ARIA and keyboard handling yourself.

**Theming:** override these CSS variables theme-wide via `ThemeProvider` or per instance via `style`; each falls back to a built-in default:

| Variable | Falls back to |
|----------|---------------|
| `--tree-node-height` | `32px` |
| `--tree-selected-background` | `var(--primary-color)` |
| `--tree-selected-color` | `var(--text-on-primary)` |

**Global defaults:** adopts `ConfigProvider`, e.g. `defaultProps={{ Tree: { indent: 30 } }}`. See [ConfigProvider.md](ConfigProvider.md).

**Slots:** `root` `node` `toggle` `icon` `label` `empty`

**Labels:** `empty` — the text this component renders of its own. Pass `labels` to override any of them, on the component or app-wide through `ConfigProvider`; keys merge individually. Type: `TreeLabels`.
