Primitives
Tree
Headless tree component with expandable nodes, selection support, and full template customization. Perfect for file explorers, navigation menus, and nested lists.
At a glance
- Expandable — Toggle nodes open/closed
- Selection — Single & multi-select
- Custom Templates — Full control over rendering
- Lazy Loading — Fetch children per level, once
- Headless — You bring the styles
Live examples
Loading demo…
Code
Basic Tree
const nodes: TreeNode[] = [
{
id: 'src',
label: 'src',
children: [
{ id: 'app', label: 'app.component.ts' },
{ id: 'main', label: 'main.ts' },
],
},
{ id: 'package', label: 'package.json' },
];
<qz-tree [nodes]="nodes" />
Pre-expanded Nodes
const nodes: TreeNode[] = [
{
id: 'docs',
label: 'docs',
expanded: true, // starts expanded
children: [
{ id: 'readme', label: 'README.md' },
{ id: 'license', label: 'LICENSE' },
],
},
];
<qz-tree [nodes]="nodes" />
Custom Node Template
// Project an <ng-template> into <qz-tree>, or pass one via [nodeTemplate].
// Quartz's <qz-tree-node> host already owns role="treeitem", tabindex, every aria-*,
// indentation, and click/keydown/focus — the template only supplies content/visuals.
// Don't add role, tabindex, aria-* or a row-level (click) handler here; that would
// duplicate what the host already provides.
<qz-tree [nodes]="nodes">
<ng-template let-node let-expanded="expanded" let-hasChildren="hasChildren" let-toggle="toggle">
@if (hasChildren) {
<span (click)="toggle(); $event.stopPropagation()">{{ expanded ? '▼' : '▶' }}</span>
}
{{ hasChildren ? (expanded ? '📂' : '📁') : '📄' }} {{ node.label }}
</ng-template>
</qz-tree>
Lazy Loading (per level)
// Only the first level is in memory; children arrive per expand.
readonly nodes: TreeNode[] = [
{ id: 'assets', label: 'assets/', hasChildren: true },
{ id: 'robots.txt', label: 'robots.txt' },
];
readonly loadChildren: TreeLoadChildrenFn = async (node) => {
const res = await fetch(`/api/list?prefix=${node.id}`);
if (!res.ok) throw new Error(res.statusText);
return res.json(); // TreeNode[]
};
<qz-tree [nodes]="nodes" [loadChildren]="loadChildren" [nodeTemplate]="tpl" />
<!-- aria-busy already comes from the host while loading() is true — the template only
needs to render content/visuals, same as any other custom node template. -->
<ng-template
#tpl
let-node
let-expanded="expanded"
let-hasChildren="hasChildren"
let-loading="loading"
let-error="error"
let-toggle="toggle"
let-retry="retry"
>
@if (hasChildren) {
<button (click)="error() ? retry() : toggle()">
{{ loading() ? '…' : error() ? '↻' : expanded ? '▼' : '▶' }}
</button>
}
{{ node.label }}
@if (error()) { <span>{{ $any(error()).message }}</span> }
</ng-template>
API Usage
treeService.expandAll();
treeService.collapseAll();
treeService.toggle('node-id');
treeService.select('node-id');
treeService.clearSelection();