# Tree

> Hierarchical list with expand / collapse, selection (single or multi), and full keyboard navigation. Renders inline (display widget) or as a `wr-select`-style combobox via `openOn="overlay"` (form-bound picker — replaces the standalone `wr-tree-select`).

Source: https://ngwr.dev/reference/components/tree  
Kind: Composite

## Installation

```angular-ts
import { WrTree, type WrTreeNode } from 'ngwr/tree';

@Component({ imports: [WrTree] })
export class MyComponent {
  protected readonly nodes: readonly WrTreeNode[] = [
    { id: 'src', label: 'src', children: [{ id: 'src/app.ts', label: 'app.ts' }] },
  ];
}
```

## Single selection

Click a row to select. Arrow keys navigate, ←/→ collapse/expand, Enter or Space selects, Home/End jump to ends.

```angular-html
<wr-tree
  [nodes]="folders"
  [(selected)]="picked"
  [(expanded)]="open"
  selectionMode="single"
/>
```

## Multi selection

Cmd / Ctrl + click toggles individual selections in multi mode.

```angular-html
<wr-tree
  [nodes]="folders"
  [(selected)]="picked"
  selectionMode="multi"
/>

<!-- Cmd / Ctrl + click toggles individual selections. -->
```

## Combobox mode — single

`openOn="overlay"` turns the tree into a `<wr-select>`-style trigger that opens a popover. Bind the `value` model with `[(value)]` or `[formField]`; classic `[(ngModel)]` / `[formControl]` keep working. Replaces the standalone `wr-tree-select`.

```angular-html
<!-- Combobox shape — opens an overlay containing the tree.
     A signal-forms native control (FormValueControl). Replaces wr-tree-select. -->
<wr-tree
  openOn="overlay"
  [nodes]="folders"
  selectionMode="single"
  placeholder="Pick a folder"
  [(value)]="picked"
/>
```

## Combobox mode — multi + chips

`maxTagCount` caps visible chips before collapsing into `+N more`. `defaultExpandAll` auto-expands every parent on first open.

```angular-html
<wr-tree
  openOn="overlay"
  [nodes]="folders"
  selectionMode="multi"
  [maxTagCount]="2"
  [defaultExpandAll]="true"
  placeholder="Pick folders"
  [(value)]="picked"
/>
```

## Virtual scroll

Set `virtualScroll` to window a large tree — only ~one viewport of rows stays in the DOM. `rowHeight` (0 = auto-measure the first row) and `viewportHeight` size the window; keyboard nav switches to `aria-activedescendant` so Arrow / Home / End / Enter keep working across un-rendered rows. Works inline and in `openOn=overlay`. The tree below holds 5,020 nodes.

```angular-html
<!-- 5,000+ nodes; only ~one viewport of rows stays in the DOM. -->
<wr-tree
  [nodes]="bigTree"
  [(expanded)]="expanded"
  [(selected)]="picked"
  virtualScroll
  [viewportHeight]="320"
/>
```

## API

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `nodes` | Tree data. | `readonly WrTreeNode<TId>[]` | `[]` |
| `selected` | Selected node ids (two-way bindable). | `readonly TId[]` | `[]` |
| `expanded` | Expanded node ids (two-way bindable). | `readonly TId[]` | `[]` |
| `selectionMode` | Selection behavior. | `WrTreeSelectionMode` | `'single'` |
| `disabled` | Disable the whole tree. Bound automatically from the field's disabled state when used with `[formField]`. | `boolean` | `false` |
| `readonly` | Refuse selection changes while the tree stays focusable and the value still submits. Bound automatically from the field's readonly state when used with `[formField]`. Expanding and collapsing keep working: a branch is NAVIGATION, not a value, so a read-only tree is still browsable — it is picking and un-picking that stops. In `overlay` mode the trigger additionally refuses to open, because there the panel exists only to choose from and it already shows what is chosen. `aria-readonly` rides on the combobox trigger only; role `tree` does not support the state, so the inline shape has nothing valid to mirror it onto. | `boolean` | `false` |
| `openOn` | Render shape. | `'inline' \| 'overlay'` | `'inline'` |
| `placeholder` | Placeholder shown on the trigger when no node is selected. | `string` | `''` |
| `ariaLabel` | Accessible name of the overlay trigger. Falls back to the placeholder, then to `select.label` — a `role="combobox"` with nothing selected and no placeholder otherwise has no name at all. | `string \| null` | `null` |
| `clearable` | Show a clear-all (×) button on the trigger when at least one node is selected. | `boolean` | `true` |
| `maxTagCount` | Cap on the number of chips rendered on the trigger before collapsing the rest into `+N more` (multi mode only). `0` = render every chip. | `number` | `0` |
| `defaultExpandAll` | Auto-expand every node that has children on first open of the overlay. | `boolean` | `false` |
| `virtualScroll` | Window the visible-node list so a large tree keeps only ~one viewport of rows in the DOM. Opt-in and OFF by default — a tree without it renders byte-identically to today. Works in both `inline` and `overlay` shapes; while on, keyboard navigation switches to the `aria-activedescendant` pattern so Arrow / Home / End / Enter keep working across un-rendered rows. | `boolean` | `false` |
| `rowHeight` | Uniform row height in px used to map scroll offset to node index. `0` (default) measures the first rendered row once and reuses it, so it adapts to the active density / touch target automatically. Read only when `virtualScroll` is on. | `number` | `0` |
| `viewportHeight` | Height of the scroll viewport when `virtualScroll` is on — a number (px) or any CSS length (`'60vh'`). A numeric px value lets the server prerender the exact first window. | `number \| string` | `288` |
| `overscan` | Extra rows kept rendered above and below the viewport as scroll headroom. | `number` | `6` |
| `value` | Form value — the current selection as seen by a bound field. Bound by `[formField]`, or two-way via `[(value)]`. Shape follows `selectionMode`: `TId \| null` in single mode, `readonly TId[]` in multi mode. Works in both `openOn` modes; `[(selected)]` stays the inline-native API and always carries an array, whatever the selection mode. | `unknown` | `undefined` |
| `(touch)` | Emitted on blur so a bound field can mark itself touched. | `void` | — |

## Types

| Name | Description | Type | Default |
| --- | --- | --- | --- |
| `WrTreeNode` | `{ id, label, children?, disabled?, icon? }`. `id` is what `[(selected)]` / `[(expanded)]` carry; a node without `children` is a leaf. | `interface` | `—` |

## CSS variables

Custom properties `ngwr/tree` publishes. Each default below is declared on the component's own selector, so a `:root` override is shadowed by it — set them on that selector, on a wrapper you scope yourself, or inline on the element. Unlike the BEM class names, these are the supported way to restyle the component.

| Variable | Default | Declared on |
| --- | --- | --- |
| `--wr-tree-indent` | `1.25rem` | `.wr-tree` |
| `--wr-tree-panel-max-height` | `18rem` only under `.wr-tree--combobox` — unset elsewhere | `.wr-tree--combobox` |
| `--wr-tree-trigger-bg` | `var(--wr-color-surface)` only under `.wr-tree--combobox` — unset elsewhere | `.wr-tree--combobox` +1 variant override |
| `--wr-tree-trigger-border` | `var(--wr-color-outline)` only under `.wr-tree--combobox` — unset elsewhere | `.wr-tree--combobox` |
| `--wr-tree-trigger-color` | `var(--wr-color-on-surface)` only under `.wr-tree--combobox` — unset elsewhere | `.wr-tree--combobox` +1 variant override |
| `--wr-tree-trigger-font-size` | `var(--wr-control-font-size-md)` only under `.wr-tree--combobox` — unset elsewhere | `.wr-tree--combobox` |
| `--wr-tree-trigger-line-height` | `var(--wr-control-line-height-md)` only under `.wr-tree--combobox` — unset elsewhere | `.wr-tree--combobox` |
| `--wr-tree-trigger-min-width` | `12rem` only under `.wr-tree--combobox` — unset elsewhere | `.wr-tree--combobox` |
| `--wr-tree-trigger-padding-x` | `var(--wr-control-padding-x-md)` only under `.wr-tree--combobox` — unset elsewhere | `.wr-tree--combobox` |
| `--wr-tree-trigger-padding-y` | `var(--wr-control-padding-y-md)` only under `.wr-tree--combobox` — unset elsewhere | `.wr-tree--combobox` |
| `--wr-tree-trigger-radius` | `var(--wr-control-radius-md)` only under `.wr-tree--combobox` — unset elsewhere | `.wr-tree--combobox` |
