Docs
/
Angular
Chapter 14

14 — Angular Material & CDK

What is Angular Material?

Angular Material is a UI component library implementing Google's Material Design for Angular. The CDK (Component Dev Kit) provides behavior primitives without styling.

ng add @angular/material
# Selects a theme, typography, animations

Setup

// app.config.ts
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';

export const appConfig: ApplicationConfig = {
  providers: [
    provideAnimationsAsync(),   // Required for Material animations
    provideRouter(routes),
  ],
};

Import Components Individually (Standalone)

import { MatButtonModule } from '@angular/material/button';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatTableModule } from '@angular/material/table';
import { MatDialogModule } from '@angular/material/dialog';
import { MatSnackBarModule } from '@angular/material/snack-bar';

@Component({
  imports: [
    MatButtonModule,
    MatInputModule,
    MatFormFieldModule,
  ],
})

Common Components

Buttons

<button mat-button>Basic</button>
<button mat-raised-button color="primary">Raised</button>
<button mat-flat-button color="accent">Flat</button>
<button mat-stroked-button>Outlined</button>
<button mat-icon-button><mat-icon>edit</mat-icon></button>
<button mat-fab><mat-icon>add</mat-icon></button>
<button mat-mini-fab><mat-icon>add</mat-icon></button>

Form Fields & Inputs

<mat-form-field appearance="outline">
  <mat-label>Email</mat-label>
  <input matInput formControlName="email" type="email">
  <mat-icon matSuffix>email</mat-icon>
  <mat-hint>We'll never share your email</mat-hint>
  <mat-error>Valid email required</mat-error>
</mat-form-field>

<mat-form-field appearance="outline">
  <mat-label>Role</mat-label>
  <mat-select formControlName="role">
    @for (role of roles; track role) {
      <mat-option [value]="role">{{ role | titlecase }}</mat-option>
    }
  </mat-select>
</mat-form-field>

<mat-form-field>
  <mat-label>Bio</mat-label>
  <textarea matInput formControlName="bio" rows="4"></textarea>
  <mat-hint align="end">{{ bio.value.length }} / 500</mat-hint>
</mat-form-field>

Table

@Component({
  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="actions">
        <th mat-header-cell *matHeaderCellDef>Actions</th>
        <td mat-cell *matCellDef="let user">
          <button mat-icon-button (click)="edit(user)"><mat-icon>edit</mat-icon></button>
          <button mat-icon-button color="warn" (click)="delete(user)"><mat-icon>delete</mat-icon></button>
        </td>
      </ng-container>

      <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
      <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
    </table>

    <mat-paginator [pageSizeOptions]="[10, 25, 50]" showFirstLastButtons />
  `,
})
export class UserTableComponent implements AfterViewInit {
  displayedColumns = ['name', 'email', 'actions'];
  dataSource = new MatTableDataSource<User>([]);

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

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

  applyFilter(value: string) {
    this.dataSource.filter = value.trim().toLowerCase();
  }
}

Dialog

// dialog component
@Component({
  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-flat-button color="warn" [mat-dialog-close]="true">Delete</button>
    </mat-dialog-actions>
  `,
})
export class ConfirmDialogComponent {
  data = inject<{ name: string }>(MAT_DIALOG_DATA);
}

// Opening the dialog
private dialog = inject(MatDialog);

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

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

Snackbar (Toast)

private snackBar = inject(MatSnackBar);

showSuccess() {
  this.snackBar.open('User saved successfully!', 'Close', {
    duration: 3000,
    horizontalPosition: 'end',
    verticalPosition: 'top',
    panelClass: ['snackbar-success'],
  });
}

Theming

Custom Theme (styles.scss)

@use '@angular/material' as mat;

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

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

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

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

Dark Mode Toggle

@Injectable({ providedIn: 'root' })
export class ThemeService {
  isDark = signal(false);

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

CDK — Behavior Primitives

Drag & Drop

import { CdkDragDrop, moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';

@Component({
  imports: [DragDropModule],
  template: `
    <div cdkDropList (cdkDropListDropped)="drop($event)">
      @for (item of items; track item.id) {
        <div cdkDrag class="drag-item">{{ item.name }}</div>
      }
    </div>
  `,
})
export class SortableListComponent {
  items = [{ id: 1, name: 'Item A' }, { id: 2, name: 'Item B' }, { id: 3, name: 'Item C' }];

  drop(event: CdkDragDrop<typeof this.items>) {
    moveItemInArray(this.items, event.previousIndex, event.currentIndex);
  }
}

Virtual Scrolling (CDK)

import { ScrollingModule } from '@angular/cdk/scrolling';

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

Overlay (CDK)

import { Overlay, OverlayModule } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';

// Create floating overlay programmatically
private overlay = inject(Overlay);

openTooltip(origin: ElementRef) {
  const positionStrategy = this.overlay.position()
    .flexibleConnectedTo(origin)
    .withPositions([{ originX: 'center', originY: 'bottom', overlayX: 'center', overlayY: 'top' }]);

  const overlayRef = this.overlay.create({ positionStrategy, hasBackdrop: true });
  const portal = new ComponentPortal(TooltipComponent);
  overlayRef.attach(portal);

  overlayRef.backdropClick().subscribe(() => overlayRef.dispose());
}

Key Takeaways

  • Import Material components individually — don't import everything
  • Use MatTableDataSource with MatSort and MatPaginator for powerful tables
  • MatDialog returns an observable from afterClosed() — perfect for confirmation flows
  • Use CDK for behavior without Material styling: drag-drop, virtual scroll, overlay
  • Virtual scrolling (cdk-virtual-scroll-viewport) is essential for large lists (1000+ items)
  • Create a custom theme with m2-define-light-theme and m2-define-dark-theme
  • Prefer provideAnimationsAsync() over provideAnimations() for lazy-loaded animations