Advanced Topics

Angular Material

Build beautiful, accessible Angular UIs with Angular Material — installation, components, theming, and customization.

What is Angular Material?

Angular Material is a UI component library that implements Google's Material Design for Angular applications. It provides:

  • 35+ pre-built, accessible components
  • Built-in theming system
  • Responsive layouts with CDK
  • Consistent design language

Installation

bash
ng add @angular/material

This command will:

  1. Install @angular/material and @angular/cdk
  2. Let you choose a prebuilt theme
  3. Set up global typography and animations
  4. Add the Roboto font and Material Icons

Importing Components

Import components individually in your standalone components:

typescript
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';

@Component({
  selector: 'app-dashboard',
  standalone: true,
  imports: [MatButtonModule, MatCardModule, MatIconModule],
  template: `...`,
})
export class DashboardComponent {}

Common Components

Buttons

html
<!-- Basic buttons -->
<button mat-button>Basic</button>
<button mat-raised-button>Raised</button>
<button mat-flat-button>Flat</button>
<button mat-stroked-button>Stroked</button>

<!-- Colored buttons -->
<button mat-raised-button color="primary">Primary</button>
<button mat-raised-button color="accent">Accent</button>
<button mat-raised-button color="warn">Warn</button>

<!-- Icon button -->
<button mat-icon-button>
  <mat-icon>favorite</mat-icon>
</button>

<!-- FAB -->
<button mat-fab>
  <mat-icon>add</mat-icon>
</button>

Cards

html
<mat-card>
  <mat-card-header>
    <mat-card-title>Angular Material</mat-card-title>
    <mat-card-subtitle>UI Component Library</mat-card-subtitle>
  </mat-card-header>

  <img mat-card-image src="angular.png" alt="Angular">

  <mat-card-content>
    <p>Build beautiful Angular apps with Material Design components.</p>
  </mat-card-content>

  <mat-card-actions align="end">
    <button mat-button>LEARN MORE</button>
    <button mat-raised-button color="primary">GET STARTED</button>
  </mat-card-actions>
</mat-card>

Forms / Inputs

html
<mat-form-field appearance="outline">
  <mat-label>Email</mat-label>
  <input matInput type="email" [formControl]="emailControl">
  <mat-icon matSuffix>email</mat-icon>
  <mat-hint>Enter your email address</mat-hint>
  <mat-error>Please enter a valid email</mat-error>
</mat-form-field>

<mat-form-field appearance="outline">
  <mat-label>Country</mat-label>
  <mat-select [formControl]="countryControl">
    <mat-option value="us">United States</mat-option>
    <mat-option value="uk">United Kingdom</mat-option>
    <mat-option value="ca">Canada</mat-option>
  </mat-select>
</mat-form-field>

Toolbar and Sidenav

html
<mat-sidenav-container>
  <mat-sidenav mode="side" opened>
    <mat-nav-list>
      <a mat-list-item routerLink="/dashboard" routerLinkActive="active">
        <mat-icon matListItemIcon>dashboard</mat-icon>
        <span matListItemTitle>Dashboard</span>
      </a>
      <a mat-list-item routerLink="/users" routerLinkActive="active">
        <mat-icon matListItemIcon>people</mat-icon>
        <span matListItemTitle>Users</span>
      </a>
    </mat-nav-list>
  </mat-sidenav>

  <mat-sidenav-content>
    <mat-toolbar color="primary">
      <span>My Application</span>
      <span class="spacer"></span>
      <button mat-icon-button><mat-icon>notifications</mat-icon></button>
      <button mat-icon-button><mat-icon>account_circle</mat-icon></button>
    </mat-toolbar>

    <router-outlet></router-outlet>
  </mat-sidenav-content>
</mat-sidenav-container>

Table

typescript
@Component({
  standalone: true,
  imports: [MatTableModule, MatSortModule, MatPaginatorModule],
  template: `
    <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 user">{{ user.name }}</td>
      </ng-container>

      <ng-container matColumnDef="email">
        <th mat-header-cell *matHeaderCellDef mat-sort-header>Email</th>
        <td mat-cell *matCellDef="let user">{{ user.email }}</td>
      </ng-container>

      <ng-container matColumnDef="role">
        <th mat-header-cell *matHeaderCellDef>Role</th>
        <td mat-cell *matCellDef="let user">
          <mat-chip [color]="user.role === 'admin' ? 'primary' : 'default'">
            {{ user.role }}
          </mat-chip>
        </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, 25]" showFirstLastButtons></mat-paginator>
  `,
})
export class UserTableComponent implements AfterViewInit {
  displayedColumns = ['name', 'email', 'role'];
  dataSource = new MatTableDataSource<User>([]);

  @ViewChild(MatSort) sort!: MatSort;
  @ViewChild(MatPaginator) paginator!: MatPaginator;

  ngAfterViewInit() {
    this.dataSource.sort = this.sort;
    this.dataSource.paginator = this.paginator;
  }
}

Dialog

typescript
// dialog component
@Component({
  standalone: true,
  imports: [MatDialogModule, MatButtonModule],
  template: `
    <h2 mat-dialog-title>Confirm Delete</h2>
    <mat-dialog-content>
      Are you sure you want to delete "{{ data.name }}"?
    </mat-dialog-content>
    <mat-dialog-actions align="end">
      <button mat-button mat-dialog-close>Cancel</button>
      <button mat-raised-button color="warn" [mat-dialog-close]="true">Delete</button>
    </mat-dialog-actions>
  `,
})
export class ConfirmDialogComponent {
  data = inject(MAT_DIALOG_DATA);
}

// Open from another component
export class UserListComponent {
  private dialog = inject(MatDialog);

  confirmDelete(user: User) {
    const ref = this.dialog.open(ConfirmDialogComponent, {
      data: { name: user.name },
      width: '400px',
    });

    ref.afterClosed().subscribe(confirmed => {
      if (confirmed) this.deleteUser(user.id);
    });
  }
}

Snackbar (Toast Notifications)

typescript
export class NotificationService {
  private snackBar = inject(MatSnackBar);

  success(message: string) {
    this.snackBar.open(message, 'Close', {
      duration: 3000,
      panelClass: 'snack-success',
      horizontalPosition: 'end',
      verticalPosition: 'top',
    });
  }

  error(message: string) {
    this.snackBar.open(message, 'Dismiss', {
      duration: 5000,
      panelClass: 'snack-error',
    });
  }
}

Theming

Custom Theme

Create a custom theme in styles.scss:

scss
@use '@angular/material' as mat;

// Define custom palettes
$my-primary: mat.m2-define-palette(mat.$m2-indigo-palette);
$my-accent: mat.m2-define-palette(mat.$m2-pink-palette, A200, A100, A400);
$my-warn: mat.m2-define-palette(mat.$m2-red-palette);

// Create theme
$my-theme: mat.m2-define-light-theme((
  color: (
    primary: $my-primary,
    accent: $my-accent,
    warn: $my-warn,
  ),
  typography: mat.m2-define-typography-config(),
  density: 0,
));

// Apply theme globally
@include mat.all-component-themes($my-theme);

// Dark theme variant
.dark-theme {
  $dark-theme: mat.m2-define-dark-theme((
    color: (
      primary: $my-primary,
      accent: $my-accent,
      warn: $my-warn,
    ),
  ));
  @include mat.all-component-colors($dark-theme);
}

Theme Toggle

typescript
@Component({
  template: `
    <button mat-icon-button (click)="toggleTheme()">
      <mat-icon>{{ isDark ? 'light_mode' : 'dark_mode' }}</mat-icon>
    </button>
  `,
})
export class ThemeToggleComponent {
  isDark = false;

  toggleTheme() {
    this.isDark = !this.isDark;
    document.body.classList.toggle('dark-theme', this.isDark);
  }
}

CDK (Component Dev Kit)

The CDK provides behavior primitives without styling:

FeatureUse Case
DragDropModuleDrag and drop lists
OverlayModuleCustom popups, tooltips
ScrollingModuleVirtual scrolling for large lists
A11yModuleFocus traps, live announcer
ClipboardModuleCopy to clipboard
LayoutModuleResponsive breakpoint detection
typescript
// Virtual scrolling for performance
import { ScrollingModule } from '@angular/cdk/scrolling';

@Component({
  imports: [ScrollingModule],
  template: `
    <cdk-virtual-scroll-viewport itemSize="48" class="list-container">
      <div *cdkVirtualFor="let item of items" class="list-item">
        {{ item.name }}
      </div>
    </cdk-virtual-scroll-viewport>
  `,
  styles: [`.list-container { height: 400px; }`],
})
export class VirtualListComponent {
  items = Array.from({ length: 10000 }, (_, i) => ({ name: `Item ${i}` }));
}

Next Steps

With polished UIs using Angular Material, let's learn about testing your Angular application — unit tests, component tests, and integration tests using Jasmine and Karma.