Composite

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

Installation

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.

<wr-tree
  [nodes]="folders"
  [(selected)]="picked"
  [(expanded)]="open"
  selectionMode="single"
/>
  • src
  • app
  • app.ts
  • main.ts
  • styles.scss
  • package.json
  • tsconfig.json
Selected: (none)

Multi selection

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

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

<!-- Cmd / Ctrl + click toggles individual selections. -->
  • package.json
  • tsconfig.json
Selected: (none)

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.

<!-- 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"
/>
Picked: (none)

Combobox mode — multi + chips

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

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

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.

<!-- 5,000+ nodes; only ~one viewport of rows stays in the DOM. -->
<wr-tree
  [nodes]="bigTree"
  [(expanded)]="expanded"
  [(selected)]="picked"
  virtualScroll
  [viewportHeight]="320"
/>
  • Group 1
  • Item 1.1
  • Item 1.2
  • Item 1.3
  • Item 1.4
  • Item 1.5
  • Item 1.6
  • Item 1.7
  • Item 1.8
  • Item 1.9
  • Item 1.10
  • Item 1.11
  • Item 1.12
  • Item 1.13
  • Item 1.14
  • Item 1.15
  • Item 1.16
Picked: (none)

API

NameDescriptionTypeDefault
nodesTree data.readonly WrTreeNode<TId>[][]
selectedSelected node ids (two-way bindable).readonly TId[][]
expandedExpanded node ids (two-way bindable).readonly TId[][]
selectionModeSelection behavior.WrTreeSelectionMode'single'
disabledDisable the whole tree. Bound automatically from the field's disabled state when used with [formField].booleanfalse
readonlyRefuse 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.booleanfalse
openOnRender shape.'inline' | 'overlay''inline'
placeholderPlaceholder shown on the trigger when no node is selected.string''
ariaLabelAccessible 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 | nullnull
clearableShow a clear-all (×) button on the trigger when at least one node is selected.booleantrue
maxTagCountCap on the number of chips rendered on the trigger before collapsing the rest into +N more (multi mode only). 0 = render every chip.number0
defaultExpandAllAuto-expand every node that has children on first open of the overlay.booleanfalse
virtualScrollWindow 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.booleanfalse
rowHeightUniform 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.number0
viewportHeightHeight 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 | string288
overscanExtra rows kept rendered above and below the viewport as scroll headroom.number6
valueForm 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.unknownundefined
(touch)Emitted on blur so a bound field can mark itself touched.void

Types

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

VariableDefaultDeclared on
--wr-tree-indent1.25rem.wr-tree
--wr-tree-panel-max-height18rem only under .wr-tree--combobox — unset elsewhere.wr-tree--combobox
--wr-tree-trigger-bgvar(--wr-color-surface) only under .wr-tree--combobox — unset elsewhere.wr-tree--combobox +1 variant override
--wr-tree-trigger-bordervar(--wr-color-outline) only under .wr-tree--combobox — unset elsewhere.wr-tree--combobox
--wr-tree-trigger-colorvar(--wr-color-on-surface) only under .wr-tree--combobox — unset elsewhere.wr-tree--combobox +1 variant override
--wr-tree-trigger-font-sizevar(--wr-control-font-size-md) only under .wr-tree--combobox — unset elsewhere.wr-tree--combobox
--wr-tree-trigger-line-heightvar(--wr-control-line-height-md) only under .wr-tree--combobox — unset elsewhere.wr-tree--combobox
--wr-tree-trigger-min-width12rem only under .wr-tree--combobox — unset elsewhere.wr-tree--combobox
--wr-tree-trigger-padding-xvar(--wr-control-padding-x-md) only under .wr-tree--combobox — unset elsewhere.wr-tree--combobox
--wr-tree-trigger-padding-yvar(--wr-control-padding-y-md) only under .wr-tree--combobox — unset elsewhere.wr-tree--combobox
--wr-tree-trigger-radiusvar(--wr-control-radius-md) only under .wr-tree--combobox — unset elsewhere.wr-tree--combobox