Tree
Overview
A tree displays hierarchical data where items can expand to reveal children or collapse to hide them. Users navigate with arrow keys, expand and collapse nodes, and optionally select items for navigation or data selection scenarios.
TS
import {Component, signal} from '@angular/core';import {NgTemplateOutlet} from '@angular/common';import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';type TreeNode = { name: string; value: string; children?: TreeNode[]; disabled?: boolean; expanded?: boolean;};@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],})export class App { readonly nodes: TreeNode[] = [ { name: 'public', value: 'public', children: [ {name: 'index.html', value: 'public/index.html'}, {name: 'favicon.ico', value: 'public/favicon.ico'}, {name: 'styles.css', value: 'public/styles.css'}, ], expanded: true, }, { name: 'src', value: 'src', children: [ { name: 'app', value: 'src/app', children: [ {name: 'app.component.ts', value: 'src/app/app.component.ts'}, {name: 'app.module.ts', value: 'src/app/app.module.ts', disabled: true}, {name: 'app.css', value: 'src/app/app.css'}, ], expanded: false, }, { name: 'assets', value: 'src/assets', children: [{name: 'logo.png', value: 'src/assets/logo.png'}], expanded: false, }, { name: 'environments', value: 'src/environments', children: [ { name: 'environment.prod.ts', value: 'src/environments/environment.prod.ts', expanded: false, }, {name: 'environment.ts', value: 'src/environments/environment.ts'}, ], expanded: false, }, {name: 'main.ts', value: 'src/main.ts'}, {name: 'polyfills.ts', value: 'src/polyfills.ts'}, {name: 'styles.css', value: 'src/styles.css', disabled: true}, {name: 'test.ts', value: 'src/test.ts'}, ], expanded: false, }, {name: 'angular.json', value: 'angular.json'}, {name: 'package.json', value: 'package.json'}, {name: 'README.md', value: 'README.md'}, ]; readonly selected = signal(['angular.json']);}
HTML
<ul ngTree #tree="ngTree" [(values)]="selected" class="basic-tree"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: nodes, parent: tree}" /></ul><ng-template #treeNodes let-nodes="nodes" let-parent="parent"> @for (node of nodes; track node.value) { <li ngTreeItem [parent]="parent" [value]="node.value" [label]="node.name" [disabled]="node.disabled" [(expanded)]="node.expanded" #treeItem="ngTreeItem" > <span aria-hidden="true" class="material-symbols-outlined expand-icon" >{{node.children ? 'chevron_right' : ''}}</span > <span aria-hidden="true" class="material-symbols-outlined" >{{node.children ? 'folder' : 'docs'}}</span > {{ node.name }} <span aria-hidden="true" class="material-symbols-outlined selected-icon">check</span> </li> @if (node.children) { <ul role="group"> <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: node.children, parent: group}" /> </ng-template> </ul> } }</ng-template>
CSS
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; user-select: none; font-family: var(--inter-font);}[ngTree] { min-width: 24rem; background-color: var(--septenary-contrast); border-radius: 0.5rem; padding: 0.5rem;}[ngTreeItem] { cursor: pointer; list-style: none; text-decoration: none; display: flex; align-items: center; gap: 1rem; padding: 0.3rem 1rem;}[ngTreeItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}[ngTreeItem]:focus,[ngTreeItem]:hover { background-color: var(--quinary-contrast);}[ngTreeItem]:focus { outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);}[ngTreeItem][aria-selected='true'],[ngTreeItem][aria-selected='true'] .expand-icon { background-image: var(--pink-to-purple-horizontal-gradient); background-clip: text; color: transparent;}.material-symbols-outlined { margin: 0; width: 24px;}.expand-icon { transition: transform 0.2s ease;}[ngTreeItem][aria-expanded='true'] .expand-icon { transform: rotate(90deg);}.selected-icon { visibility: hidden; margin-left: auto;}[ngTreeItem][aria-current] .selected-icon,[ngTreeItem][aria-selected='true'] .selected-icon { visibility: visible;}li[aria-expanded='false'] + ul[role='group'] { display: none;}
Usage
Trees work well for displaying hierarchical data where users need to navigate through nested structures.
Use trees when:
- Building file system navigation
- Showing folder and document hierarchies
- Creating nested menu structures
- Displaying organization charts
- Browsing hierarchical data
- Implementing site navigation with nested sections
Avoid trees when:
- Displaying flat lists (use Listbox instead)
- Showing data tables (use Grid instead)
- Creating simple dropdowns (use Select instead)
- Building breadcrumb navigation (use breadcrumb patterns)
Features
- Hierarchical navigation - Nested tree structure with expand and collapse functionality
- Selection modes - Single or multi-selection with explicit or follow-focus behavior
- Selection follows focus - Optional automatic selection when focus changes
- Keyboard navigation - Arrow keys, Home, End, and type-ahead search
- Expand/collapse - Right/Left arrows or Enter to toggle parent nodes
- Disabled items - Disable specific nodes with focus management
- Focus modes - Roving tabindex or activedescendant focus strategies
- RTL support - Right-to-left language navigation
Examples
Navigation tree
Use a tree for navigation where clicking items triggers actions rather than selecting them.
TS
import {Component, signal} from '@angular/core';import {NgTemplateOutlet} from '@angular/common';import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';type TreeNode = { name: string; value: string; icon: string; children?: TreeNode[]; disabled?: boolean; expanded?: boolean;};@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],})export class App { readonly nodes: TreeNode[] = [ { name: 'Inbox', value: 'inbox', icon: 'inbox', }, { name: 'Sent', value: 'sent', icon: 'send', }, { name: 'Drafts', value: 'drafts', icon: 'draft', }, { name: 'Spam', value: 'spam', icon: 'report', }, { name: 'Trash', value: 'trash', icon: 'delete', }, { name: 'Labels', value: 'labels', expanded: true, icon: 'label', children: [ {name: 'Personal', value: 'folders/personal', icon: 'label'}, {name: 'Work', value: 'folders/work', icon: 'label'}, {name: 'Travel', value: 'folders/travel', icon: 'label'}, {name: 'Receipts', value: 'folders/receipts', icon: 'label'}, ], }, ]; readonly selected = signal(['inbox']);}
HTML
<ul ngTree #tree="ngTree" [nav]="true" [(values)]="selected" class="basic-tree"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: nodes, parent: tree}" /></ul><ng-template #treeNodes let-nodes="nodes" let-parent="parent"> @for (node of nodes; track node.value) { <a ngTreeItem [parent]="parent" [value]="node.value" [label]="node.name" [disabled]="node.disabled" [selectable]="!node.children" [(expanded)]="node.expanded" #treeItem="ngTreeItem" href="#{{ node.name }}" (click)="$event.preventDefault()" > <span aria-hidden="true" class="material-symbols-outlined">{{node.icon}}</span> {{ node.name }} <span aria-hidden="true" class="material-symbols-outlined expand-icon" >{{node.children ? 'keyboard_arrow_up' : ''}}</span > </a> @if (node.children) { <ul role="group"> <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: node.children, parent: group}" /> </ng-template> </ul> } }</ng-template>
CSS
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; user-select: none; font-family: var(--inter-font);}[ngTree] { min-width: 24rem; background-color: var(--septenary-contrast); border-radius: 0.5rem; padding: 0.5rem;}[ngTreeItem] { cursor: pointer; list-style: none; text-decoration: none; display: flex; align-items: center; gap: 1rem; padding: 0.3rem 1rem; color: var(--primary-contrast);}[ngTreeItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}[ngTreeItem]:focus,[ngTreeItem]:hover { background-color: var(--quinary-contrast);}[ngTreeItem]:focus { outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);}[ngTreeItem][aria-current] { background-image: var(--pink-to-purple-horizontal-gradient); background-clip: text; color: transparent;}.material-symbols-outlined { margin: 0; width: 24px;}.expand-icon { transition: transform 0.2s ease;}[ngTreeItem][aria-expanded='true'] .expand-icon { transform: rotate(180deg);}li[aria-expanded='false'] + ul[role='group'] { display: none;}
Set [nav]="true" to enable navigation mode. This uses aria-current to indicate the current page instead of selection.
Single selection
Enable single selection for scenarios where users choose one item from the tree.
TS
import {Component, signal} from '@angular/core';import {NgTemplateOutlet} from '@angular/common';import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';type TreeNode = { name: string; value: string; children?: TreeNode[]; disabled?: boolean; expanded?: boolean;};@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],})export class App { readonly nodes: TreeNode[] = [ { name: 'public', value: 'public', children: [ {name: 'index.html', value: 'public/index.html'}, {name: 'favicon.ico', value: 'public/favicon.ico'}, {name: 'styles.css', value: 'public/styles.css'}, ], expanded: true, }, { name: 'src', value: 'src', children: [ { name: 'app', value: 'src/app', children: [ {name: 'app.component.ts', value: 'src/app/app.component.ts'}, {name: 'app.module.ts', value: 'src/app/app.module.ts', disabled: true}, {name: 'app.css', value: 'src/app/app.css'}, ], expanded: false, }, { name: 'assets', value: 'src/assets', children: [{name: 'logo.png', value: 'src/assets/logo.png'}], expanded: false, }, { name: 'environments', value: 'src/environments', children: [ { name: 'environment.prod.ts', value: 'src/environments/environment.prod.ts', expanded: false, }, {name: 'environment.ts', value: 'src/environments/environment.ts'}, ], expanded: false, }, {name: 'main.ts', value: 'src/main.ts'}, {name: 'polyfills.ts', value: 'src/polyfills.ts'}, {name: 'styles.css', value: 'src/styles.css', disabled: true}, {name: 'test.ts', value: 'src/test.ts'}, ], expanded: false, }, {name: 'angular.json', value: 'angular.json'}, {name: 'package.json', value: 'package.json'}, {name: 'README.md', value: 'README.md'}, ]; readonly selected = signal(['angular.json']);}
HTML
<ul ngTree #tree="ngTree" [(values)]="selected" class="basic-tree"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: nodes, parent: tree}" /></ul><ng-template #treeNodes let-nodes="nodes" let-parent="parent"> @for (node of nodes; track node.value) { <li ngTreeItem [parent]="parent" [value]="node.value" [label]="node.name" [disabled]="node.disabled" [(expanded)]="node.expanded" #treeItem="ngTreeItem" > <span aria-hidden="true" class="material-symbols-outlined expand-icon" >{{node.children ? 'chevron_right' : ''}}</span > <span aria-hidden="true" class="material-symbols-outlined" >{{node.children ? 'folder' : 'docs'}}</span > {{ node.name }} <span aria-hidden="true" class="material-symbols-outlined selected-icon">check</span> </li> @if (node.children) { <ul role="group"> <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: node.children, parent: group}" /> </ng-template> </ul> } }</ng-template>
CSS
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; user-select: none; font-family: var(--inter-font);}[ngTree] { min-width: 24rem; background-color: var(--septenary-contrast); border-radius: 0.5rem; padding: 0.5rem;}[ngTreeItem] { cursor: pointer; list-style: none; text-decoration: none; display: flex; align-items: center; gap: 1rem; padding: 0.3rem 1rem;}[ngTreeItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}[ngTreeItem]:focus,[ngTreeItem]:hover { background-color: var(--quinary-contrast);}[ngTreeItem]:focus { outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);}[ngTreeItem][aria-selected='true'],[ngTreeItem][aria-selected='true'] .expand-icon { background-image: var(--pink-to-purple-horizontal-gradient); background-clip: text; color: transparent;}.material-symbols-outlined { margin: 0; width: 24px;}.expand-icon { transition: transform 0.2s ease;}[ngTreeItem][aria-expanded='true'] .expand-icon { transform: rotate(90deg);}.selected-icon { visibility: hidden; margin-left: auto;}[ngTreeItem][aria-current] .selected-icon,[ngTreeItem][aria-selected='true'] .selected-icon { visibility: visible;}li[aria-expanded='false'] + ul[role='group'] { display: none;}
TS
import {Component, computed, signal} from '@angular/core';import {NgTemplateOutlet} from '@angular/common';import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';type TreeNode = { name: string; value: string; children?: TreeNode[]; disabled?: boolean; expanded?: boolean;};@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],})export class App { readonly nodes: readonly TreeNode[] = [ { name: 'C:', value: 'C:', expanded: true, children: [ { name: 'Program Files/', value: 'C:/Program Files', children: [ {name: 'Common Files', value: 'C:/Program Files/Common Files'}, {name: 'Internet Explorer', value: 'C:/Program Files/Internet Explorer'}, ], expanded: false, }, { name: 'Users/', value: 'C:/Users', children: [ {name: 'Default', value: 'C:/Users/Default'}, {name: 'Public', value: 'C:/Users/Public'}, ], expanded: false, }, { name: 'Windows/', value: 'C:/Windows', children: [ {name: 'System32', value: 'C:/Windows/System32'}, {name: 'Web', value: 'C:/Windows/Web'}, ], expanded: false, }, {name: 'pagefile.sys', value: 'C:/pagefile.sys'}, {name: 'swapfile.sys', value: 'C:/swapfile.sys', disabled: true}, ], }, ]; readonly selected = signal([]); readonly selectedCount = computed(() => this.selected().length);}
HTML
<div class="win95-file-explorer"> <div class="title-bar"> <div class="title-bar-text">Exploring - (C:)</div> <div class="title-bar-controls"> <button tabindex="-1" aria-label="Minimize"><span>-</span></button> <button tabindex="-1" aria-label="Maximize"><span>□</span></button> <button tabindex="-1" aria-label="Close"><span>×</span></button> </div> </div> <div class="menu-bar"> <div class="menu-item"><u>F</u>ile</div> <div class="menu-item"><u>E</u>dit</div> <div class="menu-item"><u>V</u>iew</div> <div class="menu-item"><u>T</u>ools</div> <div class="menu-item"><u>H</u>elp</div> </div> <div class="toolbar"> <button tabindex="-1" class="win95-btn"><span class="icon">←</span> Back</button> <button tabindex="-1" class="win95-btn"><span class="icon">→</span> Forward</button> <button tabindex="-1" class="win95-btn"><span class="icon">↑</span> Up</button> </div> <div class="tree-view"> <ul ngTree #tree="ngTree" class="retro-tree" [(values)]="selected"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: nodes, parent: tree}" /> </ul> </div> <div class="status-bar"> <div class="status-panel status-panel-grow">{{selectedCount()}} object(s) selected</div> <div class="status-panel status-panel-right">4.5MB</div> </div></div><ng-template #treeNodes let-nodes="nodes" let-parent="parent"> @for (node of nodes; track node.value) { <li ngTreeItem [parent]="parent" [value]="node.value" [label]="node.name" [(expanded)]="node.expanded" [disabled]="node.disabled" #treeItem="ngTreeItem" > <span aria-hidden="true"> @if (node.children) { {{treeItem.expanded() ? '📂' : '📁'}} } @else { 📄 } </span> {{ node.name }} <span aria-hidden="true" class="material-symbols-outlined selected-icon">check</span> </li> @if (node.children) { <ul role="group"> <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: node.children, parent: group}" /> </ng-template> </ul> } }</ng-template>
CSS
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');@import url('https://fonts.googleapis.com/css2?family=Jersey+20&display=swap');:host { display: flex; justify-content: center; user-select: none; --win95-gray: #c0c0c0; --win95-dark-gray: #808080; --win95-light: #ffffff; --win95-shadow: #000000; --win95-blue: #000080; --win95-active-blue: linear-gradient(to right, #000080, #1084d0); --win95-font: "Jersey 20", sans-serif; font-family: var(--win95-font); font-size: 1.1rem;}.win95-file-explorer { border-style: solid; border-width: 1px; border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light); box-shadow: 1px 1px 0 var(--win95-shadow); background-color: var(--win95-gray); min-width: 350px;}.win95-btn { background-color: var(--win95-gray); padding: 3px 8px; cursor: pointer; border-style: solid; border-width: 1px; border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light); box-shadow: 1px 1px 0 var(--win95-shadow);}.win95-btn:active { box-shadow: 1px 1px 0 var(--win95-shadow) inset; padding: 4px 7px 2px 9px; border-style: solid; border-width: 1px; border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);}.title-bar { background: var(--win95-active-blue); color: var(--win95-light); padding: 3px 6px; height: 20px; display: flex; justify-content: space-between; align-items: center;}.title-bar-text { font-weight: bold;}.title-bar-controls button { background: var(--win95-gray); border: 1px solid var(--win95-light); color: var(--win95-shadow); width: 16px; height: 14px; line-height: 8px; font-size: 14px; padding: 0; margin-left: 1px; border-style: solid; border-width: 1px; border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light); box-shadow: 1px 1px 0 var(--win95-shadow);}.title-bar-controls button span { display: block; margin-top: -3px;}.menu-bar { display: flex; background-color: var(--win95-gray); border-bottom: 1px solid var(--win95-dark-gray); padding: 1px 2px;}.menu-item { padding: 0 6px; cursor: default; margin-right: 4px;}.menu-item:hover { background-color: var(--win95-blue); color: var(--win95-light);}.toolbar { display: flex; align-items: center; padding: 4px; border-top: 1px solid var(--win95-light); border-bottom: 1px solid var(--win95-dark-gray);}.toolbar .win95-btn { display: flex; align-items: center; margin-right: 8px;}.toolbar .icon { font-size: 1.25rem; line-height: 1; margin-right: 4px;}.tree-view { background-color: var(--win95-light); min-height: 300px; padding: 4px; border-width: 1px; border-style: solid; border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray); color: var(--win95-shadow);}.status-bar { margin-top: 2px;}.status-panel-grow { flex-grow: 1;}.status-panel-right { text-align: right;}.status-bar { height: 18px; background-color: var(--win95-gray); border-top: 1px solid var(--win95-light); border-left: 1px solid var(--win95-light); border-right: 1px solid var(--win95-dark-gray); border-bottom: 1px solid var(--win95-dark-gray); display: flex; font-size: 14px;}.status-panel { padding: 0 4px; height: 100%; display: flex; align-items: center; margin-right: 1px; border-style: solid; border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray); border-width: 1px; flex-grow: 1;}.status-panel:last-child { flex-grow: 0; width: 120px;}[ngTree] { padding: 0; margin: 0;}[ngTreeItem] { cursor: pointer; list-style: none; text-decoration: none; display: flex; align-items: center; gap: 0.75rem; padding: 0.25rem 0.75rem;}[ngTreeItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}[ngTreeItem]:focus,[ngTreeItem]:hover { background-color: var(--senary-contrast); color: var(--primary-contrast); outline: none;}[ngTreeItem][aria-selected='true'] { background-color: var(--win95-blue); color: var(--win95-light);}.selected-icon { visibility: hidden; margin-left: auto;}[ngTreeItem][aria-current] .selected-icon,[ngTreeItem][aria-selected='true'] .selected-icon { visibility: visible;}li[aria-expanded='false'] + ul[role='group'] { display: none;}
Leave [multi]="false" (the default) for single selection. Users press Space to select the focused item.
Multi-selection
Allow users to select multiple items from the tree.
TS
import {Component, signal} from '@angular/core';import {NgTemplateOutlet} from '@angular/common';import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';type TreeNode = { name: string; value: string; children?: TreeNode[]; disabled?: boolean; expanded?: boolean;};@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],})export class App { readonly nodes: TreeNode[] = [ { name: 'public', value: 'public', children: [ {name: 'index.html', value: 'public/index.html'}, {name: 'favicon.ico', value: 'public/favicon.ico'}, {name: 'styles.css', value: 'public/styles.css'}, ], expanded: true, }, { name: 'src', value: 'src', children: [ { name: 'app', value: 'src/app', children: [ {name: 'app.component.ts', value: 'src/app/app.component.ts'}, {name: 'app.module.ts', value: 'src/app/app.module.ts', disabled: true}, {name: 'app.css', value: 'src/app/app.css'}, ], expanded: false, }, { name: 'assets', value: 'src/assets', children: [{name: 'logo.png', value: 'src/assets/logo.png'}], expanded: false, }, { name: 'environments', value: 'src/environments', children: [ { name: 'environment.prod.ts', value: 'src/environments/environment.prod.ts', expanded: false, }, {name: 'environment.ts', value: 'src/environments/environment.ts'}, ], expanded: false, }, {name: 'main.ts', value: 'src/main.ts'}, {name: 'polyfills.ts', value: 'src/polyfills.ts'}, {name: 'styles.css', value: 'src/styles.css', disabled: true}, {name: 'test.ts', value: 'src/test.ts'}, ], expanded: false, }, {name: 'angular.json', value: 'angular.json'}, {name: 'package.json', value: 'package.json'}, {name: 'README.md', value: 'README.md'}, ]; readonly selected = signal(['angular.json', 'public/styles.css']);}
HTML
<ul ngTree #tree="ngTree" [multi]="true" [(values)]="selected" class="basic-tree"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: nodes, parent: tree}" /></ul><ng-template #treeNodes let-nodes="nodes" let-parent="parent"> @for (node of nodes; track node.value) { <li ngTreeItem [parent]="parent" [value]="node.value" [label]="node.name" [disabled]="node.disabled" [(expanded)]="node.expanded" #treeItem="ngTreeItem" > <span aria-hidden="true" class="material-symbols-outlined expand-icon" >{{node.children ? 'chevron_right' : ''}}</span > <span aria-hidden="true" class="material-symbols-outlined" >{{node.children ? 'folder' : 'docs'}}</span > {{ node.name }} <span aria-hidden="true" class="material-symbols-outlined selected-icon">check</span> </li> @if (node.children) { <ul role="group"> <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: node.children, parent: group}" /> </ng-template> </ul> } }</ng-template>
CSS
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; user-select: none; font-family: var(--inter-font);}[ngTree] { min-width: 24rem; background-color: var(--septenary-contrast); border-radius: 0.5rem; padding: 0.5rem;}[ngTreeItem] { cursor: pointer; list-style: none; text-decoration: none; display: flex; align-items: center; gap: 1rem; padding: 0.3rem 1rem;}[ngTreeItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}[ngTreeItem]:focus,[ngTreeItem]:hover { background-color: var(--quinary-contrast);}[ngTreeItem]:focus { outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);}[ngTreeItem][aria-selected='true'],[ngTreeItem][aria-selected='true'] .expand-icon { background-image: var(--pink-to-purple-horizontal-gradient); background-clip: text; color: transparent;}.material-symbols-outlined { margin: 0; width: 24px;}.expand-icon { transition: transform 0.2s ease;}[ngTreeItem][aria-expanded='true'] .expand-icon { transform: rotate(90deg);}.selected-icon { visibility: hidden; margin-left: auto;}[ngTreeItem][aria-current] .selected-icon,[ngTreeItem][aria-selected='true'] .selected-icon { visibility: visible;}li[aria-expanded='false'] + ul[role='group'] { display: none;}
TS
import {Component, computed, signal} from '@angular/core';import {NgTemplateOutlet} from '@angular/common';import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';type TreeNode = { name: string; value: string; children?: TreeNode[]; disabled?: boolean; expanded?: boolean;};@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],})export class App { readonly nodes: readonly TreeNode[] = [ { name: 'C:', value: 'C:', expanded: true, children: [ { name: 'Program Files/', value: 'C:/Program Files', children: [ {name: 'Common Files', value: 'C:/Program Files/Common Files'}, {name: 'Internet Explorer', value: 'C:/Program Files/Internet Explorer'}, ], expanded: false, }, { name: 'Users/', value: 'C:/Users', children: [ {name: 'Default', value: 'C:/Users/Default'}, {name: 'Public', value: 'C:/Users/Public'}, ], expanded: false, }, { name: 'Windows/', value: 'C:/Windows', children: [ {name: 'System32', value: 'C:/Windows/System32'}, {name: 'Web', value: 'C:/Windows/Web'}, ], expanded: false, }, {name: 'pagefile.sys', value: 'C:/pagefile.sys'}, {name: 'swapfile.sys', value: 'C:/swapfile.sys', disabled: true}, ], }, ]; readonly selected = signal([]); readonly selectedCount = computed(() => this.selected().length);}
HTML
<div class="win95-file-explorer"> <div class="title-bar"> <div class="title-bar-text">Exploring - (C:)</div> <div class="title-bar-controls"> <button tabindex="-1" aria-label="Minimize"><span>-</span></button> <button tabindex="-1" aria-label="Maximize"><span>□</span></button> <button tabindex="-1" aria-label="Close"><span>×</span></button> </div> </div> <div class="menu-bar"> <div class="menu-item"><u>F</u>ile</div> <div class="menu-item"><u>E</u>dit</div> <div class="menu-item"><u>V</u>iew</div> <div class="menu-item"><u>T</u>ools</div> <div class="menu-item"><u>H</u>elp</div> </div> <div class="toolbar"> <button tabindex="-1" class="win95-btn"><span class="icon">←</span> Back</button> <button tabindex="-1" class="win95-btn"><span class="icon">→</span> Forward</button> <button tabindex="-1" class="win95-btn"><span class="icon">↑</span> Up</button> </div> <div class="tree-view"> <ul ngTree #tree="ngTree" class="retro-tree" [(values)]="selected" [multi]="true"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: nodes, parent: tree}" /> </ul> </div> <div class="status-bar"> <div class="status-panel status-panel-grow">{{selectedCount()}} object(s) selected</div> <div class="status-panel status-panel-right">4.5MB</div> </div></div><ng-template #treeNodes let-nodes="nodes" let-parent="parent"> @for (node of nodes; track node.value) { <li ngTreeItem [parent]="parent" [value]="node.value" [label]="node.name" [(expanded)]="node.expanded" [disabled]="node.disabled" #treeItem="ngTreeItem" > <span aria-hidden="true"> @if (node.children) { {{treeItem.expanded() ? '📂' : '📁'}} } @else { 📄 } </span> {{ node.name }} <span aria-hidden="true" class="material-symbols-outlined selected-icon">check</span> </li> @if (node.children) { <ul role="group"> <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: node.children, parent: group}" /> </ng-template> </ul> } }</ng-template>
CSS
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');@import url('https://fonts.googleapis.com/css2?family=Jersey+20&display=swap');:host { display: flex; justify-content: center; user-select: none; --win95-gray: #c0c0c0; --win95-dark-gray: #808080; --win95-light: #ffffff; --win95-shadow: #000000; --win95-blue: #000080; --win95-active-blue: linear-gradient(to right, #000080, #1084d0); --win95-font: "Jersey 20", sans-serif; font-family: var(--win95-font); font-size: 1.1rem;}.win95-file-explorer { border-style: solid; border-width: 1px; border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light); box-shadow: 1px 1px 0 var(--win95-shadow); background-color: var(--win95-gray); min-width: 350px;}.win95-btn { background-color: var(--win95-gray); padding: 3px 8px; cursor: pointer; border-style: solid; border-width: 1px; border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light); box-shadow: 1px 1px 0 var(--win95-shadow);}.win95-btn:active { box-shadow: 1px 1px 0 var(--win95-shadow) inset; padding: 4px 7px 2px 9px; border-style: solid; border-width: 1px; border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);}.title-bar { background: var(--win95-active-blue); color: var(--win95-light); padding: 3px 6px; height: 20px; display: flex; justify-content: space-between; align-items: center;}.title-bar-text { font-weight: bold;}.title-bar-controls button { background: var(--win95-gray); border: 1px solid var(--win95-light); color: var(--win95-shadow); width: 16px; height: 14px; line-height: 8px; font-size: 14px; padding: 0; margin-left: 1px; border-style: solid; border-width: 1px; border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light); box-shadow: 1px 1px 0 var(--win95-shadow);}.title-bar-controls button span { display: block; margin-top: -3px;}.menu-bar { display: flex; background-color: var(--win95-gray); border-bottom: 1px solid var(--win95-dark-gray); padding: 1px 2px;}.menu-item { padding: 0 6px; cursor: default; margin-right: 4px;}.menu-item:hover { background-color: var(--win95-blue); color: var(--win95-light);}.toolbar { display: flex; align-items: center; padding: 4px; border-top: 1px solid var(--win95-light); border-bottom: 1px solid var(--win95-dark-gray);}.toolbar .win95-btn { display: flex; align-items: center; margin-right: 8px;}.toolbar .icon { font-size: 1.25rem; line-height: 1; margin-right: 4px;}.tree-view { background-color: var(--win95-light); min-height: 300px; padding: 4px; border-width: 1px; border-style: solid; border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray); color: var(--win95-shadow);}.status-bar { margin-top: 2px;}.status-panel-grow { flex-grow: 1;}.status-panel-right { text-align: right;}.status-bar { height: 18px; background-color: var(--win95-gray); border-top: 1px solid var(--win95-light); border-left: 1px solid var(--win95-light); border-right: 1px solid var(--win95-dark-gray); border-bottom: 1px solid var(--win95-dark-gray); display: flex; font-size: 14px;}.status-panel { padding: 0 4px; height: 100%; display: flex; align-items: center; margin-right: 1px; border-style: solid; border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray); border-width: 1px; flex-grow: 1;}.status-panel:last-child { flex-grow: 0; width: 120px;}[ngTree] { padding: 0; margin: 0;}[ngTreeItem] { cursor: pointer; list-style: none; text-decoration: none; display: flex; align-items: center; gap: 0.75rem; padding: 0.25rem 0.75rem;}[ngTreeItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}[ngTreeItem]:focus,[ngTreeItem]:hover { background-color: var(--senary-contrast); color: var(--primary-contrast); outline: none;}[ngTreeItem][aria-selected='true'] { background-color: var(--win95-blue); color: var(--win95-light);}.selected-icon { visibility: hidden; margin-left: auto;}[ngTreeItem][aria-current] .selected-icon,[ngTreeItem][aria-selected='true'] .selected-icon { visibility: visible;}li[aria-expanded='false'] + ul[role='group'] { display: none;}
Set [multi]="true" on the tree. Users select items individually with Space or select ranges with Shift+Arrow keys.
Selection follows focus
When selection follows focus, the focused item is automatically selected. This simplifies interaction for navigation scenarios.
TS
import {Component, signal} from '@angular/core';import {NgTemplateOutlet} from '@angular/common';import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';type TreeNode = { name: string; value: string; children?: TreeNode[]; disabled?: boolean; expanded?: boolean;};@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],})export class App { readonly nodes: TreeNode[] = [ { name: 'public', value: 'public', children: [ {name: 'index.html', value: 'public/index.html'}, {name: 'favicon.ico', value: 'public/favicon.ico'}, {name: 'styles.css', value: 'public/styles.css'}, ], expanded: true, }, { name: 'src', value: 'src', children: [ { name: 'app', value: 'src/app', children: [ {name: 'app.component.ts', value: 'src/app/app.component.ts'}, {name: 'app.module.ts', value: 'src/app/app.module.ts', disabled: true}, {name: 'app.css', value: 'src/app/app.css'}, ], expanded: false, }, { name: 'assets', value: 'src/assets', children: [{name: 'logo.png', value: 'src/assets/logo.png'}], expanded: false, }, { name: 'environments', value: 'src/environments', children: [ { name: 'environment.prod.ts', value: 'src/environments/environment.prod.ts', expanded: false, }, {name: 'environment.ts', value: 'src/environments/environment.ts'}, ], expanded: false, }, {name: 'main.ts', value: 'src/main.ts'}, {name: 'polyfills.ts', value: 'src/polyfills.ts'}, {name: 'styles.css', value: 'src/styles.css', disabled: true}, {name: 'test.ts', value: 'src/test.ts'}, ], expanded: false, }, {name: 'angular.json', value: 'angular.json'}, {name: 'package.json', value: 'package.json'}, {name: 'README.md', value: 'README.md'}, ]; readonly selected = signal(['angular.json']);}
HTML
<ul ngTree #tree="ngTree" [(values)]="selected" selectionMode="follow" class="basic-tree"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: nodes, parent: tree}" /></ul><ng-template #treeNodes let-nodes="nodes" let-parent="parent"> @for (node of nodes; track node.value) { <li ngTreeItem [parent]="parent" [value]="node.value" [label]="node.name" [disabled]="node.disabled" [(expanded)]="node.expanded" #treeItem="ngTreeItem" > <span aria-hidden="true" class="material-symbols-outlined expand-icon" >{{node.children ? 'chevron_right' : ''}}</span > <span aria-hidden="true" class="material-symbols-outlined" >{{node.children ? 'folder' : 'docs'}}</span > {{ node.name }} <span aria-hidden="true" class="material-symbols-outlined selected-icon">check</span> </li> @if (node.children) { <ul role="group"> <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: node.children, parent: group}" /> </ng-template> </ul> } }</ng-template>
CSS
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; user-select: none; font-family: var(--inter-font);}[ngTree] { min-width: 24rem; background-color: var(--septenary-contrast); border-radius: 0.5rem; padding: 0.5rem;}[ngTreeItem] { cursor: pointer; list-style: none; text-decoration: none; display: flex; align-items: center; gap: 1rem; padding: 0.3rem 1rem;}[ngTreeItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}[ngTreeItem]:focus,[ngTreeItem]:hover { background-color: var(--quinary-contrast);}[ngTreeItem]:focus { outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);}[ngTreeItem][aria-selected='true'],[ngTreeItem][aria-selected='true'] .expand-icon { background-image: var(--pink-to-purple-horizontal-gradient); background-clip: text; color: transparent;}.material-symbols-outlined { margin: 0; width: 24px;}.expand-icon { transition: transform 0.2s ease;}[ngTreeItem][aria-expanded='true'] .expand-icon { transform: rotate(90deg);}.selected-icon { visibility: hidden; margin-left: auto;}[ngTreeItem][aria-current] .selected-icon,[ngTreeItem][aria-selected='true'] .selected-icon { visibility: visible;}li[aria-expanded='false'] + ul[role='group'] { display: none;}
TS
import {Component, computed, signal} from '@angular/core';import {NgTemplateOutlet} from '@angular/common';import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';type TreeNode = { name: string; value: string; children?: TreeNode[]; disabled?: boolean; expanded?: boolean;};@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],})export class App { readonly nodes: readonly TreeNode[] = [ { name: 'C:', value: 'C:', expanded: true, children: [ { name: 'Program Files/', value: 'C:/Program Files', children: [ {name: 'Common Files', value: 'C:/Program Files/Common Files'}, {name: 'Internet Explorer', value: 'C:/Program Files/Internet Explorer'}, ], expanded: false, }, { name: 'Users/', value: 'C:/Users', children: [ {name: 'Default', value: 'C:/Users/Default'}, {name: 'Public', value: 'C:/Users/Public'}, ], expanded: false, }, { name: 'Windows/', value: 'C:/Windows', children: [ {name: 'System32', value: 'C:/Windows/System32'}, {name: 'Web', value: 'C:/Windows/Web'}, ], expanded: false, }, {name: 'pagefile.sys', value: 'C:/pagefile.sys'}, {name: 'swapfile.sys', value: 'C:/swapfile.sys', disabled: true}, ], }, ]; readonly selected = signal([]); readonly selectedCount = computed(() => this.selected().length);}
HTML
<div class="win95-file-explorer"> <div class="title-bar"> <div class="title-bar-text">Exploring - (C:)</div> <div class="title-bar-controls"> <button tabindex="-1" aria-label="Minimize"><span>-</span></button> <button tabindex="-1" aria-label="Maximize"><span>□</span></button> <button tabindex="-1" aria-label="Close"><span>×</span></button> </div> </div> <div class="menu-bar"> <div class="menu-item"><u>F</u>ile</div> <div class="menu-item"><u>E</u>dit</div> <div class="menu-item"><u>V</u>iew</div> <div class="menu-item"><u>T</u>ools</div> <div class="menu-item"><u>H</u>elp</div> </div> <div class="toolbar"> <button tabindex="-1" class="win95-btn"><span class="icon">←</span> Back</button> <button tabindex="-1" class="win95-btn"><span class="icon">→</span> Forward</button> <button tabindex="-1" class="win95-btn"><span class="icon">↑</span> Up</button> </div> <div class="tree-view"> <ul ngTree #tree="ngTree" class="retro-tree" selectionMode="follow" [(values)]="selected"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: nodes, parent: tree}" /> </ul> </div> <div class="status-bar"> <div class="status-panel status-panel-grow">{{selectedCount()}} object(s) selected</div> <div class="status-panel status-panel-right">4.5MB</div> </div></div><ng-template #treeNodes let-nodes="nodes" let-parent="parent"> @for (node of nodes; track node.value) { <li ngTreeItem [parent]="parent" [value]="node.value" [label]="node.name" [(expanded)]="node.expanded" [disabled]="node.disabled" #treeItem="ngTreeItem" > <span aria-hidden="true"> @if (node.children) { {{treeItem.expanded() ? '📂' : '📁'}} } @else { 📄 } </span> {{ node.name }} <span aria-hidden="true" class="material-symbols-outlined selected-icon">check</span> </li> @if (node.children) { <ul role="group"> <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: node.children, parent: group}" /> </ng-template> </ul> } }</ng-template>
CSS
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');@import url('https://fonts.googleapis.com/css2?family=Jersey+20&display=swap');:host { display: flex; justify-content: center; user-select: none; --win95-gray: #c0c0c0; --win95-dark-gray: #808080; --win95-light: #ffffff; --win95-shadow: #000000; --win95-blue: #000080; --win95-active-blue: linear-gradient(to right, #000080, #1084d0); --win95-font: "Jersey 20", sans-serif; font-family: var(--win95-font); font-size: 1.1rem;}.win95-file-explorer { border-style: solid; border-width: 1px; border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light); box-shadow: 1px 1px 0 var(--win95-shadow); background-color: var(--win95-gray); min-width: 350px;}.win95-btn { background-color: var(--win95-gray); padding: 3px 8px; cursor: pointer; border-style: solid; border-width: 1px; border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light); box-shadow: 1px 1px 0 var(--win95-shadow);}.win95-btn:active { box-shadow: 1px 1px 0 var(--win95-shadow) inset; padding: 4px 7px 2px 9px; border-style: solid; border-width: 1px; border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);}.title-bar { background: var(--win95-active-blue); color: var(--win95-light); padding: 3px 6px; height: 20px; display: flex; justify-content: space-between; align-items: center;}.title-bar-text { font-weight: bold;}.title-bar-controls button { background: var(--win95-gray); border: 1px solid var(--win95-light); color: var(--win95-shadow); width: 16px; height: 14px; line-height: 8px; font-size: 14px; padding: 0; margin-left: 1px; border-style: solid; border-width: 1px; border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light); box-shadow: 1px 1px 0 var(--win95-shadow);}.title-bar-controls button span { display: block; margin-top: -3px;}.menu-bar { display: flex; background-color: var(--win95-gray); border-bottom: 1px solid var(--win95-dark-gray); padding: 1px 2px;}.menu-item { padding: 0 6px; cursor: default; margin-right: 4px;}.menu-item:hover { background-color: var(--win95-blue); color: var(--win95-light);}.toolbar { display: flex; align-items: center; padding: 4px; border-top: 1px solid var(--win95-light); border-bottom: 1px solid var(--win95-dark-gray);}.toolbar .win95-btn { display: flex; align-items: center; margin-right: 8px;}.toolbar .icon { font-size: 1.25rem; line-height: 1; margin-right: 4px;}.tree-view { background-color: var(--win95-light); min-height: 300px; padding: 4px; border-width: 1px; border-style: solid; border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray); color: var(--win95-shadow);}.status-bar { margin-top: 2px;}.status-panel-grow { flex-grow: 1;}.status-panel-right { text-align: right;}.status-bar { height: 18px; background-color: var(--win95-gray); border-top: 1px solid var(--win95-light); border-left: 1px solid var(--win95-light); border-right: 1px solid var(--win95-dark-gray); border-bottom: 1px solid var(--win95-dark-gray); display: flex; font-size: 14px;}.status-panel { padding: 0 4px; height: 100%; display: flex; align-items: center; margin-right: 1px; border-style: solid; border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray); border-width: 1px; flex-grow: 1;}.status-panel:last-child { flex-grow: 0; width: 120px;}[ngTree] { padding: 0; margin: 0;}[ngTreeItem] { cursor: pointer; list-style: none; text-decoration: none; display: flex; align-items: center; gap: 0.75rem; padding: 0.25rem 0.75rem;}[ngTreeItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}[ngTreeItem]:focus,[ngTreeItem]:hover { background-color: var(--senary-contrast); color: var(--primary-contrast); outline: none;}[ngTreeItem][aria-selected='true'] { background-color: var(--win95-blue); color: var(--win95-light);}.selected-icon { visibility: hidden; margin-left: auto;}[ngTreeItem][aria-current] .selected-icon,[ngTreeItem][aria-selected='true'] .selected-icon { visibility: visible;}li[aria-expanded='false'] + ul[role='group'] { display: none;}
Set [selectionMode]="'follow'" on the tree. Selection automatically updates as users navigate with arrow keys.
Disabled tree items
Disable specific tree nodes to prevent interaction. Control whether disabled items can receive focus.
TS
import {Component, signal} from '@angular/core';import {NgTemplateOutlet} from '@angular/common';import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';type TreeNode = { name: string; value: string; children?: TreeNode[]; disabled?: boolean; expanded?: boolean;};@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],})export class App { readonly nodes: TreeNode[] = [ { name: 'public', value: 'public', children: [ {name: 'index.html', value: 'public/index.html'}, {name: 'favicon.ico', value: 'public/favicon.ico'}, {name: 'styles.css', value: 'public/styles.css'}, ], expanded: true, disabled: true, }, { name: 'src', value: 'src', children: [ { name: 'app', value: 'src/app', children: [ {name: 'app.component.ts', value: 'src/app/app.component.ts'}, {name: 'app.module.ts', value: 'src/app/app.module.ts', disabled: true}, {name: 'app.css', value: 'src/app/app.css'}, ], expanded: false, }, { name: 'assets', value: 'src/assets', children: [{name: 'logo.png', value: 'src/assets/logo.png'}], expanded: false, }, { name: 'environments', value: 'src/environments', children: [ { name: 'environment.prod.ts', value: 'src/environments/environment.prod.ts', expanded: false, }, {name: 'environment.ts', value: 'src/environments/environment.ts'}, ], expanded: false, }, {name: 'main.ts', value: 'src/main.ts'}, {name: 'polyfills.ts', value: 'src/polyfills.ts'}, {name: 'styles.css', value: 'src/styles.css', disabled: true}, {name: 'test.ts', value: 'src/test.ts'}, ], expanded: false, disabled: true, }, {name: 'angular.json', value: 'angular.json'}, {name: 'package.json', value: 'package.json'}, {name: 'README.md', value: 'README.md'}, ]; readonly selected = signal(['angular.json']);}
HTML
<ul ngTree #tree="ngTree" [(values)]="selected" class="basic-tree"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: nodes, parent: tree}" /></ul><ng-template #treeNodes let-nodes="nodes" let-parent="parent"> @for (node of nodes; track node.value) { <li ngTreeItem [parent]="parent" [value]="node.value" [label]="node.name" [disabled]="node.disabled" [(expanded)]="node.expanded" #treeItem="ngTreeItem" > <span aria-hidden="true" class="material-symbols-outlined expand-icon" >{{node.children ? 'chevron_right' : ''}}</span > <span aria-hidden="true" class="material-symbols-outlined" >{{node.children ? 'folder' : 'docs'}}</span > {{ node.name }} <span aria-hidden="true" class="material-symbols-outlined selected-icon">check</span> </li> @if (node.children) { <ul role="group"> <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: node.children, parent: group}" /> </ng-template> </ul> } }</ng-template>
CSS
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');:host { display: flex; justify-content: center; user-select: none; font-family: var(--inter-font);}[ngTree] { min-width: 24rem; background-color: var(--septenary-contrast); border-radius: 0.5rem; padding: 0.5rem;}[ngTreeItem] { cursor: pointer; list-style: none; text-decoration: none; display: flex; align-items: center; gap: 1rem; padding: 0.3rem 1rem;}[ngTreeItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}[ngTreeItem]:focus,[ngTreeItem]:hover { background-color: var(--quinary-contrast);}[ngTreeItem]:focus { outline: 1px solid color-mix(in srgb, var(--hot-pink) 60%, transparent);}[ngTreeItem][aria-selected='true'],[ngTreeItem][aria-selected='true'] .expand-icon { background-image: var(--pink-to-purple-horizontal-gradient); background-clip: text; color: transparent;}.material-symbols-outlined { margin: 0; width: 24px;}.expand-icon { transition: transform 0.2s ease;}[ngTreeItem][aria-expanded='true'] .expand-icon { transform: rotate(90deg);}.selected-icon { visibility: hidden; margin-left: auto;}[ngTreeItem][aria-current] .selected-icon,[ngTreeItem][aria-selected='true'] .selected-icon { visibility: visible;}li[aria-expanded='false'] + ul[role='group'] { display: none;}
TS
import {Component, computed, signal} from '@angular/core';import {NgTemplateOutlet} from '@angular/common';import {Tree, TreeItem, TreeItemGroup} from '@angular/aria/tree';type TreeNode = { name: string; value: string; children?: TreeNode[]; disabled?: boolean; expanded?: boolean;};@Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrl: 'app.component.css', imports: [Tree, TreeItem, TreeItemGroup, NgTemplateOutlet],})export class App { readonly nodes: readonly TreeNode[] = [ { name: 'C:', value: 'C:', expanded: true, children: [ { name: 'Program Files/', value: 'C:/Program Files', children: [ {name: 'Common Files', value: 'C:/Program Files/Common Files'}, {name: 'Internet Explorer', value: 'C:/Program Files/Internet Explorer'}, ], expanded: true, disabled: true, }, { name: 'Users/', value: 'C:/Users', children: [ {name: 'Default', value: 'C:/Users/Default'}, {name: 'Public', value: 'C:/Users/Public'}, ], expanded: false, }, { name: 'Windows/', value: 'C:/Windows', children: [ {name: 'System32', value: 'C:/Windows/System32'}, {name: 'Web', value: 'C:/Windows/Web'}, ], expanded: false, }, {name: 'pagefile.sys', value: 'C:/pagefile.sys'}, {name: 'swapfile.sys', value: 'C:/swapfile.sys', disabled: true}, ], }, ]; readonly selected = signal([]); readonly selectedCount = computed(() => this.selected().length);}
HTML
<div class="win95-file-explorer"> <div class="title-bar"> <div class="title-bar-text">Exploring - (C:)</div> <div class="title-bar-controls"> <button tabindex="-1" aria-label="Minimize"><span>-</span></button> <button tabindex="-1" aria-label="Maximize"><span>□</span></button> <button tabindex="-1" aria-label="Close"><span>×</span></button> </div> </div> <div class="menu-bar"> <div class="menu-item"><u>F</u>ile</div> <div class="menu-item"><u>E</u>dit</div> <div class="menu-item"><u>V</u>iew</div> <div class="menu-item"><u>T</u>ools</div> <div class="menu-item"><u>H</u>elp</div> </div> <div class="toolbar"> <button tabindex="-1" class="win95-btn"><span class="icon">←</span> Back</button> <button tabindex="-1" class="win95-btn"><span class="icon">→</span> Forward</button> <button tabindex="-1" class="win95-btn"><span class="icon">↑</span> Up</button> </div> <div class="tree-view"> <ul ngTree #tree="ngTree" class="retro-tree" [(values)]="selected"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: nodes, parent: tree}" /> </ul> </div> <div class="status-bar"> <div class="status-panel status-panel-grow">{{selectedCount()}} object(s) selected</div> <div class="status-panel status-panel-right">4.5MB</div> </div></div><ng-template #treeNodes let-nodes="nodes" let-parent="parent"> @for (node of nodes; track node.value) { <li ngTreeItem [parent]="parent" [value]="node.value" [label]="node.name" [(expanded)]="node.expanded" [disabled]="node.disabled" #treeItem="ngTreeItem" > <span aria-hidden="true"> @if (node.children) { {{treeItem.expanded() ? '📂' : '📁'}} } @else { 📄 } </span> {{ node.name }} <span aria-hidden="true" class="material-symbols-outlined selected-icon">check</span> </li> @if (node.children) { <ul role="group"> <ng-template ngTreeItemGroup [ownedBy]="treeItem" #group="ngTreeItemGroup"> <ng-template [ngTemplateOutlet]="treeNodes" [ngTemplateOutletContext]="{nodes: node.children, parent: group}" /> </ng-template> </ul> } }</ng-template>
CSS
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');@import url('https://fonts.googleapis.com/css2?family=Jersey+20&display=swap');:host { display: flex; justify-content: center; user-select: none; --win95-gray: #c0c0c0; --win95-dark-gray: #808080; --win95-light: #ffffff; --win95-shadow: #000000; --win95-blue: #000080; --win95-active-blue: linear-gradient(to right, #000080, #1084d0); --win95-font: "Jersey 20", sans-serif; font-family: var(--win95-font); font-size: 1.1rem;}.win95-file-explorer { border-style: solid; border-width: 1px; border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light); box-shadow: 1px 1px 0 var(--win95-shadow); background-color: var(--win95-gray); min-width: 350px;}.win95-btn { background-color: var(--win95-gray); padding: 3px 8px; cursor: pointer; border-style: solid; border-width: 1px; border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light); box-shadow: 1px 1px 0 var(--win95-shadow);}.win95-btn:active { box-shadow: 1px 1px 0 var(--win95-shadow) inset; padding: 4px 7px 2px 9px; border-style: solid; border-width: 1px; border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray);}.title-bar { background: var(--win95-active-blue); color: var(--win95-light); padding: 3px 6px; height: 20px; display: flex; justify-content: space-between; align-items: center;}.title-bar-text { font-weight: bold;}.title-bar-controls button { background: var(--win95-gray); border: 1px solid var(--win95-light); color: var(--win95-shadow); width: 16px; height: 14px; line-height: 8px; font-size: 14px; padding: 0; margin-left: 1px; border-style: solid; border-width: 1px; border-color: var(--win95-light) var(--win95-dark-gray) var(--win95-dark-gray) var(--win95-light); box-shadow: 1px 1px 0 var(--win95-shadow);}.title-bar-controls button span { display: block; margin-top: -3px;}.menu-bar { display: flex; background-color: var(--win95-gray); border-bottom: 1px solid var(--win95-dark-gray); padding: 1px 2px;}.menu-item { padding: 0 6px; cursor: default; margin-right: 4px;}.menu-item:hover { background-color: var(--win95-blue); color: var(--win95-light);}.toolbar { display: flex; align-items: center; padding: 4px; border-top: 1px solid var(--win95-light); border-bottom: 1px solid var(--win95-dark-gray);}.toolbar .win95-btn { display: flex; align-items: center; margin-right: 8px;}.toolbar .icon { font-size: 1.25rem; line-height: 1; margin-right: 4px;}.tree-view { background-color: var(--win95-light); min-height: 300px; padding: 4px; border-width: 1px; border-style: solid; border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray); color: var(--win95-shadow);}.status-bar { margin-top: 2px;}.status-panel-grow { flex-grow: 1;}.status-panel-right { text-align: right;}.status-bar { height: 18px; background-color: var(--win95-gray); border-top: 1px solid var(--win95-light); border-left: 1px solid var(--win95-light); border-right: 1px solid var(--win95-dark-gray); border-bottom: 1px solid var(--win95-dark-gray); display: flex; font-size: 14px;}.status-panel { padding: 0 4px; height: 100%; display: flex; align-items: center; margin-right: 1px; border-style: solid; border-color: var(--win95-dark-gray) var(--win95-light) var(--win95-light) var(--win95-dark-gray); border-width: 1px; flex-grow: 1;}.status-panel:last-child { flex-grow: 0; width: 120px;}[ngTree] { padding: 0; margin: 0;}[ngTreeItem] { cursor: pointer; list-style: none; text-decoration: none; display: flex; align-items: center; gap: 0.75rem; padding: 0.25rem 0.75rem;}[ngTreeItem][aria-disabled='true'] { opacity: 0.5; cursor: default;}[ngTreeItem]:focus,[ngTreeItem]:hover { background-color: var(--senary-contrast); color: var(--primary-contrast); outline: none;}[ngTreeItem][aria-selected='true'] { background-color: var(--win95-blue); color: var(--win95-light);}.selected-icon { visibility: hidden; margin-left: auto;}[ngTreeItem][aria-current] .selected-icon,[ngTreeItem][aria-selected='true'] .selected-icon { visibility: visible;}li[aria-expanded='false'] + ul[role='group'] { display: none;}
When [softDisabled]="true" on the tree, disabled items can receive focus but cannot be activated or selected. When [softDisabled]="false", disabled items are skipped during keyboard navigation.
APIs
Tree
The container directive that manages hierarchical navigation and selection.
Inputs
| Property | Type | Default | Description |
|---|---|---|---|
disabled |
boolean |
false |
Disables the entire tree |
softDisabled |
boolean |
true |
When true, disabled items are focusable but not interactive |
multi |
boolean |
false |
Whether multiple items can be selected |
selectionMode |
'explicit' | 'follow' |
'explicit' |
Whether selection requires explicit action or follows focus |
nav |
boolean |
false |
Whether the tree is in navigation mode (uses aria-current) |
wrap |
boolean |
true |
Whether keyboard navigation wraps from last to first item |
focusMode |
'roving' | 'activedescendant' |
'roving' |
Focus strategy used by the tree |
values |
any[] |
[] |
Selected item values (supports two-way binding) |
Methods
| Method | Parameters | Description |
|---|---|---|
expandAll |
none | Expands all tree nodes |
collapseAll |
none | Collapses all tree nodes |
selectAll |
none | Selects all items (only in multi-select mode) |
clearSelection |
none | Clears all selection |
TreeItem
An individual node in the tree that can contain child nodes.
Inputs
| Property | Type | Default | Description |
|---|---|---|---|
value |
any |
— | Required. Unique value for this tree item |
disabled |
boolean |
false |
Disables this item |
expanded |
boolean |
false |
Whether the node is expanded (supports two-way binding) |
Signals
| Property | Type | Description |
|---|---|---|
selected |
Signal<boolean> |
Whether the item is selected |
active |
Signal<boolean> |
Whether the item currently has focus |
hasChildren |
Signal<boolean> |
Whether the item has child nodes |
Methods
| Method | Parameters | Description |
|---|---|---|
expand |
none | Expands this node |
collapse |
none | Collapses this node |
toggle |
none | Toggles the expansion state |
TreeGroup
A container for child tree items.
This directive has no inputs, outputs, or methods. It serves as a container to organize child ngTreeItem elements:
<li ngTreeItem value="parent"> Parent Item <ul ngTreeGroup> <li ngTreeItem value="child1">Child 1</li> <li ngTreeItem value="child2">Child 2</li> </ul></li>