Skip to main content

MatTree

The mat-tree provides a Material Design styled tree component for displaying hierarchical data with expandable and collapsible nodes.

Basic Usage

import { MatTreeModule } from '@angular/material/tree';
import { NestedTreeControl } from '@angular/cdk/tree';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';

interface FoodNode {
  name: string;
  children?: FoodNode[];
}

const TREE_DATA: FoodNode[] = [
  {
    name: 'Fruit',
    children: [{name: 'Apple'}, {name: 'Banana'}, {name: 'Orange'}],
  },
  {
    name: 'Vegetables',
    children: [{name: 'Carrot'}, {name: 'Lettuce'}],
  },
];

@Component({
  selector: 'tree-example',
  imports: [MatTreeModule, MatIconModule, MatButtonModule],
  template: `
    <mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
      <mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
        <button mat-icon-button disabled></button>
        {{ node.name }}
      </mat-tree-node>
      
      <mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
        <div class="mat-tree-node">
          <button mat-icon-button matTreeNodeToggle>
            <mat-icon>
              {{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
            </mat-icon>
          </button>
          {{ node.name }}
        </div>
        <div [class.tree-invisible]="!treeControl.isExpanded(node)" role="group">
          <ng-container matTreeNodeOutlet></ng-container>
        </div>
      </mat-nested-tree-node>
    </mat-tree>
  `,
  styles: [`
    .tree-invisible {
      display: none;
    }
  `]
})
export class TreeExample {
  treeControl = new NestedTreeControl<FoodNode>(node => node.children);
  dataSource = TREE_DATA;
  hasChild = (_: number, node: FoodNode) => !!node.children && node.children.length > 0;
}

API Reference

MatTree

Selector: mat-tree Extends: CdkTree

Inputs

NameTypeDescription
dataSourceDataSource<T> | Observable<T[]> | T[]Data source for the tree
treeControlTreeControl<T>Tree control for managing tree state

MatTreeNode

Directive: mat-tree-node Represents a node in the tree.

Inputs

NameTypeDefaultDescription
disabledbooleanfalseWhether the node is disabled
tabIndexnumber0Tabindex of the node

MatNestedTreeNode

Directive: mat-nested-tree-node Represents a nested node in the tree with children.

Inputs

NameTypeDefaultDescription
disabledbooleanfalseWhether the node is disabled
tabIndexnumber0Tabindex of the node

MatTreeNodeDef

Directive: [matTreeNodeDef] Defines the template for tree nodes:
<mat-tree-node *matTreeNodeDef="let node">{{ node.name }}</mat-tree-node>

MatTreeNodeToggle

Directive: matTreeNodeToggle Toggle for expanding/collapsing tree nodes.

MatTreeNodeOutlet

Directive: matTreeNodeOutlet Outlet for nested tree nodes.

MatTreeNodePadding

Directive: matTreeNodePadding Adds indentation padding to tree nodes based on level.

Examples

Flat Tree

import { FlatTreeControl } from '@angular/cdk/tree';
import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree';

interface FoodNode {
  name: string;
  children?: FoodNode[];
}

interface FlatNode {
  expandable: boolean;
  name: string;
  level: number;
}

@Component({
  template: `
    <mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
      <mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
        <button mat-icon-button disabled></button>
        {{ node.name }}
      </mat-tree-node>
      
      <mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
        <button mat-icon-button matTreeNodeToggle
                [attr.aria-label]="'Toggle ' + node.name">
          <mat-icon>
            {{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
          </mat-icon>
        </button>
        {{ node.name }}
      </mat-tree-node>
    </mat-tree>
  `
})
export class FlatTreeExample {
  private transformer = (node: FoodNode, level: number): FlatNode => {
    return {
      expandable: !!node.children && node.children.length > 0,
      name: node.name,
      level: level,
    };
  };

  treeControl = new FlatTreeControl<FlatNode>(
    node => node.level,
    node => node.expandable,
  );

  treeFlattener = new MatTreeFlattener(
    this.transformer,
    node => node.level,
    node => node.expandable,
    node => node.children,
  );

  dataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);

  constructor() {
    this.dataSource.data = TREE_DATA;
  }

  hasChild = (_: number, node: FlatNode) => node.expandable;
}

Checkable Tree

import { SelectionModel } from '@angular/cdk/collections';
import { MatCheckboxModule } from '@angular/material/checkbox';

@Component({
  imports: [MatTreeModule, MatCheckboxModule, MatIconModule, MatButtonModule],
  template: `
    <mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
      <mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
        <button mat-icon-button disabled></button>
        <mat-checkbox [checked]="checklistSelection.isSelected(node)"
                      (change)="todoItemSelectionToggle(node)">
          {{ node.name }}
        </mat-checkbox>
      </mat-tree-node>
      
      <mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
        <button mat-icon-button matTreeNodeToggle>
          <mat-icon>
            {{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
          </mat-icon>
        </button>
        <mat-checkbox [checked]="descendantsAllSelected(node)"
                      [indeterminate]="descendantsPartiallySelected(node)"
                      (change)="todoItemSelectionToggle(node)">
          {{ node.name }}
        </mat-checkbox>
      </mat-tree-node>
    </mat-tree>
  `
})
export class CheckableTreeExample {
  treeControl = new FlatTreeControl<FlatNode>(
    node => node.level,
    node => node.expandable,
  );
  
  checklistSelection = new SelectionModel<FlatNode>(true);
  
  // ... implementation of selection methods
  
  todoItemSelectionToggle(node: FlatNode): void {
    this.checklistSelection.toggle(node);
    const descendants = this.treeControl.getDescendants(node);
    this.checklistSelection.isSelected(node)
      ? this.checklistSelection.select(...descendants)
      : this.checklistSelection.deselect(...descendants);
  }
  
  descendantsAllSelected(node: FlatNode): boolean {
    const descendants = this.treeControl.getDescendants(node);
    return descendants.length > 0 && 
           descendants.every(child => this.checklistSelection.isSelected(child));
  }
  
  descendantsPartiallySelected(node: FlatNode): boolean {
    const descendants = this.treeControl.getDescendants(node);
    const result = descendants.some(child => this.checklistSelection.isSelected(child));
    return result && !this.descendantsAllSelected(node);
  }
}

Dynamic Tree

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable()
export class DynamicDatabase {
  dataChange = new BehaviorSubject<DynamicFlatNode[]>([]);
  
  get data(): DynamicFlatNode[] {
    return this.dataChange.value;
  }
  
  initialize() {
    const data = this.buildFileTree(TREE_DATA, 0);
    this.dataChange.next(data);
  }
  
  buildFileTree(obj: {[key: string]: any}, level: number): DynamicFlatNode[] {
    return Object.keys(obj).reduce<DynamicFlatNode[]>((accumulator, key) => {
      const value = obj[key];
      const node = new DynamicFlatNode();
      node.item = key;

      if (value != null) {
        if (typeof value === 'object') {
          node.children = this.buildFileTree(value, level + 1);
        } else {
          node.item = value;
        }
      }

      return accumulator.concat(node);
    }, []);
  }
}

@Component({
  template: `
    <mat-tree [dataSource]="dataSource" [treeControl]="treeControl">
      <mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
        <button mat-icon-button disabled></button>
        {{ node.item }}
      </mat-tree-node>
      
      <mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
        <button mat-icon-button
                [attr.aria-label]="'Toggle ' + node.item"
                (click)="loadChildren(node)"
                matTreeNodeToggle>
          <mat-icon>
            {{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}
          </mat-icon>
        </button>
        {{ node.item }}
        <mat-progress-bar *ngIf="node.isLoading" mode="indeterminate"></mat-progress-bar>
      </mat-tree-node>
    </mat-tree>
  `,
  providers: [DynamicDatabase]
})
export class DynamicTreeExample {
  treeControl: FlatTreeControl<DynamicFlatNode>;
  dataSource: DynamicDataSource;
  
  constructor(database: DynamicDatabase) {
    this.treeControl = new FlatTreeControl<DynamicFlatNode>(
      this.getLevel,
      this.isExpandable
    );
    this.dataSource = new DynamicDataSource(this.treeControl, database);
    database.initialize();
  }
  
  getLevel = (node: DynamicFlatNode) => node.level;
  isExpandable = (node: DynamicFlatNode) => node.expandable;
  hasChild = (_: number, nodeData: DynamicFlatNode) => nodeData.expandable;
  
  loadChildren(node: DynamicFlatNode) {
    if (this.treeControl.isExpanded(node)) {
      // Load children dynamically
    }
  }
}

Accessibility

Keyboard Navigation

  • UP_ARROW: Move focus to previous node
  • DOWN_ARROW: Move focus to next node
  • LEFT_ARROW: Collapse node or move to parent
  • RIGHT_ARROW: Expand node or move to first child
  • HOME: Move to first node
  • END: Move to last node
  • ENTER or SPACE: Activate/select node

ARIA

The tree has role="tree" and nodes have role="treeitem". Use appropriate labels:
<mat-tree aria-label="File system navigator">
  <mat-tree-node>...</mat-tree-node>
</mat-tree>

Screen Readers

Provide clear labels for toggle buttons:
<button mat-icon-button matTreeNodeToggle
        [attr.aria-label]="'Toggle ' + node.name">
  <mat-icon>chevron_right</mat-icon>
</button>

Styling

// Tree node indentation
mat-tree-node {
  min-height: 48px;
  padding-left: 24px;
}

// Nested node indentation
.mat-tree-node {
  &[aria-level="1"] { padding-left: 24px; }
  &[aria-level="2"] { padding-left: 48px; }
  &[aria-level="3"] { padding-left: 72px; }
}

// Hide nested children when collapsed
.tree-invisible {
  display: none;
}

// Node hover state
mat-tree-node:hover {
  background-color: rgba(0, 0, 0, 0.04);
}

// Selected node
mat-tree-node.selected {
  background-color: rgba(63, 81, 181, 0.1);
}

Data Sources

Array Data Source

const data = [/* tree data */];
this.dataSource = data;

Observable Data Source

const data$ = of([/* tree data */]);
this.dataSource = data$;

MatTreeFlatDataSource

import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree';

const treeFlattener = new MatTreeFlattener(
  transformer,
  getLevel,
  isExpandable,
  getChildren
);

this.dataSource = new MatTreeFlatDataSource(this.treeControl, treeFlattener);
this.dataSource.data = TREE_DATA;

Best Practices

  1. Clear hierarchy: Use consistent indentation
  2. Visual feedback: Show expand/collapse state clearly
  3. Performance: Use flat tree for large datasets
  4. Loading states: Show progress for dynamic loading
  5. Keyboard support: Ensure full keyboard navigation
  6. ARIA labels: Provide descriptive labels

Theming

@use '@angular/material' as mat;

$theme: mat.define-theme((
  color: (
    theme-type: light,
    primary: mat.$violet-palette,
  ),
));

html {
  @include mat.tree-theme($theme);
}

Types

TreeControl

interface TreeControl<T> {
  dataNodes: T[];
  expansionModel: SelectionModel<T>;
  getLevel: (dataNode: T) => number;
  isExpandable: (dataNode: T) => boolean;
  getChildren: (dataNode: T) => Observable<T[]> | T[] | undefined | null;
}

Build docs developers (and LLMs) love