Angular Material

This section documents modern, standalone Angular — signals, the built-in @if / @for / @switch control flow, @defer, typed reactive forms, provideHttpClient, functional guards and interceptors, and server-side rendering with hydration — as described by the official documentation at angular.dev, which is the reference these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against angular.dev before being relied on in production. Angular ships a major release roughly every six months and its APIs continue to evolve: the examples here target the current major release; where a consulted source disagrees with the current documentation, the documentation wins and the difference is noted.

This section’s bibliography lists the reference material consulted while preparing these pages.

Angular Material is the first-party Material Design component library, built on the Angular CDK. This page covers installing and theming it, the components you reach for most, and the CDK underneath. The broader survey of other UI libraries lives on Styling and UI Libraries.

Install and theme

ng add installs the packages, adds Roboto and the Material icon font, sets up a theme, and enables animations.

ng add @angular/material

Theming uses the Sass mat.theme mixin with Material 3 design tokens. Put it in the global stylesheet; a light-dark() color scheme gives you dark mode for free when the OS requests it.

// src/styles.scss
@use '@angular/material' as mat;

html {
  color-scheme: light dark;

  @include mat.theme((
    color: (
      primary: mat.$violet-palette,
      tertiary: mat.$orange-palette,
    ),
    typography: Roboto,
    density: 0,
  ));
}

// component-level override
.dense-section {
  @include mat.form-field-density(-3);
}

Read component styles with mat.get-theme-color(…​), and see the theming guide and the component docs for the full token list.

Common components

Every Material component is standalone — import the ones a template uses. A form with a field, a select, and a checkbox:

import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatCheckboxModule } from '@angular/material/checkbox';

@Component({
  selector: 'app-profile-form',
  imports: [
    ReactiveFormsModule, MatFormFieldModule, MatInputModule,
    MatSelectModule, MatCheckboxModule,
  ],
  templateUrl: './profile-form.html',
})
export class ProfileForm {
  readonly name = new FormControl('');
  readonly role = new FormControl('dev');
  readonly subscribe = new FormControl(false);
}
<mat-form-field>
  <mat-label>Name</mat-label>
  <input matInput [formControl]="name" />
</mat-form-field>

<mat-form-field>
  <mat-label>Role</mat-label>
  <mat-select [formControl]="role">
    <mat-option value="dev">Developer</mat-option>
    <mat-option value="pm">Product manager</mat-option>
  </mat-select>
</mat-form-field>

<mat-checkbox [formControl]="subscribe">Email me updates</mat-checkbox>

A datepicker needs a date adapter provider (provideNativeDateAdapter() in the app config) plus the module:

<mat-form-field>
  <mat-label>Starts</mat-label>
  <input matInput [matDatepicker]="picker" />
  <mat-datepicker-toggle matIconSuffix [for]="picker" />
  <mat-datepicker #picker />
</mat-form-field>

A sortable, paginated table is MatTableDataSource wired to MatSort and MatPaginator:

import { AfterViewInit, Component, viewChild } from '@angular/core';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';

interface User { name: string; role: string; }

@Component({
  selector: 'app-user-table',
  imports: [MatTableModule, MatSortModule, MatPaginatorModule],
  templateUrl: './user-table.html',
})
export class UserTable implements AfterViewInit {
  readonly displayedColumns = ['name', 'role'];
  readonly dataSource = new MatTableDataSource<User>([
    { name: 'Ada', role: 'dev' },
    { name: 'Grace', role: 'pm' },
  ]);

  private readonly sort = viewChild.required(MatSort);
  private readonly paginator = viewChild.required(MatPaginator);

  ngAfterViewInit(): void {
    this.dataSource.sort = this.sort();
    this.dataSource.paginator = this.paginator();
  }
}
<table mat-table [dataSource]="dataSource" matSort>
  <ng-container matColumnDef="name">
    <th mat-header-cell *matHeaderCellDef mat-sort-header>Name</th>
    <td mat-cell *matCellDef="let u">{{ u.name }}</td>
  </ng-container>
  <ng-container matColumnDef="role">
    <th mat-header-cell *matHeaderCellDef mat-sort-header>Role</th>
    <td mat-cell *matCellDef="let u">{{ u.role }}</td>
  </ng-container>
  <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
  <tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
<mat-paginator [pageSizeOptions]="[5, 10]" />

Dialogs and snackbars are opened from injected services; menus, tabs, and steppers are declared in the template:

import { Component, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
import { ConfirmDialog } from './confirm-dialog';

@Component({ selector: 'app-toolbar', templateUrl: './toolbar.html' })
export class Toolbar {
  private readonly dialog = inject(MatDialog);
  private readonly snackBar = inject(MatSnackBar);

  remove(): void {
    this.dialog.open(ConfirmDialog).afterClosed().subscribe((ok) => {
      if (ok) this.snackBar.open('Removed', 'Undo', { duration: 5000 });
    });
  }
}
<button mat-icon-button [matMenuTriggerFor]="menu">Menu</button>
<mat-menu #menu>
  <button mat-menu-item (click)="remove()">Delete</button>
</mat-menu>

<mat-tab-group>
  <mat-tab label="Details">…</mat-tab>
  <mat-tab label="History">…</mat-tab>
</mat-tab-group>

<mat-stepper linear>
  <mat-step label="Account">…</mat-step>
  <mat-step label="Confirm">…</mat-step>
</mat-stepper>

See the component catalogue for the full API of each.

The CDK

The Component Dev Kit is the unstyled behaviour Material is built on, usable on its own:

  • Overlay — position a floating panel (tooltips, custom dropdowns) in a managed layer.

  • Portal — render a component or template into an Overlay or any other outlet.

  • a11y — LiveAnnouncer for screen-reader announcements, cdkTrapFocus / FocusTrap to contain focus, FocusMonitor to observe focus origin.

  • Layout — BreakpointObserver to react to media queries as observables or signals.

  • Drag and drop — cdkDrag / cdkDropList with sorting and transfer between lists.

  • Scrolling — <cdk-virtual-scroll-viewport> renders only the visible rows of a large list.

import { Component } from '@angular/core';
import { CdkDrag, CdkDropList, CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { ScrollingModule } from '@angular/cdk/scrolling';

@Component({
  selector: 'app-reorder',
  imports: [CdkDropList, CdkDrag, ScrollingModule],
  template: `
    <cdk-virtual-scroll-viewport itemSize="40" style="height: 300px">
      <div
        cdkDropList
        (cdkDropListDropped)="drop($event)">
        <div *cdkVirtualFor="let item of items" cdkDrag>{{ item }}</div>
      </div>
    </cdk-virtual-scroll-viewport>
  `,
})
export class Reorder {
  items = Array.from({ length: 1000 }, (_, i) => `Item ${i}`);

  drop(event: CdkDragDrop<string[]>): void {
    moveItemInArray(this.items, event.previousIndex, event.currentIndex);
  }
}

Accessibility of interactive components

Material components ship keyboard support and ARIA roles, but the wiring around them is still yours: give every mat-form-field a mat-label or aria-label, set an accessible name on icon-only buttons, keep focus inside an open MatDialog and return it to the trigger on close, and announce out-of-band changes with LiveAnnouncer. Test every flow with the keyboard and a screen reader, and honour prefers-reduced-motion when adding your own animations.

import { Component, inject } from '@angular/core';
import { LiveAnnouncer } from '@angular/cdk/a11y';

@Component({
  selector: 'app-cart',
  template: `<button mat-button aria-label="Add to cart" (click)="add()">Add</button>`,
})
export class Cart {
  private readonly announcer = inject(LiveAnnouncer);
  add(): void {
    this.announcer.announce('Added to cart', 'polite');
  }
}

See Web Accessibility for conformance levels and validation tools, and the CDK a11y overview for the helpers above.