Skip to content
corpus.web

Angular Material

Google's official component library ships accessible Material Design 3 components with build-time theming through CSS custom properties

Baseline Angular 22.1.1
Kind Concept
View source

Modern Angular only No equivalent exists in the original 100 Days series. Written fresh for Angular v22, Angular Material v22, Material Design 3.

Lead

Lead with this: Angular Material is Google's official Angular component library — 50+ accessible, production-ready UI components built on Material Design 3. Its theming system generates CSS custom properties at build time so a single stylesheet supports both light and dark mode without runtime JavaScript.

What it is

Angular Material (@angular/material) ships pre-built implementations of the Material Design 3 component set: buttons, inputs, dialogs, navigation drawers, data tables, date pickers, and more. Each component handles accessibility, keyboard navigation, ARIA attributes, and focus management — things you'd spend days implementing correctly from scratch.

The library has three layers:

LayerPackageWhat it provides
CDK@angular/cdkPlatform primitives: overlays, drag-drop, virtual scroll, a11y, testing harnesses
Material components@angular/material50+ Material Design 3 UI components built on the CDK
Material themingSCSS mixins in @angular/materialDesign token system that generates CSS custom properties for your brand colors

Material components are standalone — import only what you use. The tree-shaker removes everything else.

How it works under the hood

Old approach — M2 static CSS classes

Before Material Design 3 (through Angular Material v14–v17), theming generated static CSS class rules at build time. You defined a Sass palette, called theming mixins, and Angular Material output pre-computed color classes for every component state:

scss
// M2 approach (v14-v17, deprecated — still works via v18 docs)@use '@angular/material' as mat;
@include mat.core();
$my-primary: mat.define-palette(mat.$indigo-palette);$my-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);$my-theme: mat.define-light-theme((  color: (primary: $my-primary, accent: $my-accent),));
@include mat.all-component-themes($my-theme);

This generated thousands of lines of CSS with hardcoded color values. Switching between light and dark required loading a different CSS bundle. Adding a third brand color meant duplicating the entire theme block.

New approach — M3 CSS custom properties (v19+)

Angular Material v3 (stable from v18, simplified API in v19) uses the browser's CSS custom properties as design tokens. The mat.theme() mixin generates CSS variable declarations, not hardcoded class rules:

scss
// M3 approach (v19+ — recommended)@use '@angular/material' as mat;
html {  color-scheme: light dark;  @include mat.theme((    color: mat.$violet-palette,    typography: Roboto,    density: 0,  ));}

What mat.theme() generates — not class rules, but CSS variable declarations:

css
/* Generated by mat.theme() — CSS custom properties (design tokens) */html {  --mat-sys-primary: light-dark(#6750a4, #d0bcff);  --mat-sys-on-primary: light-dark(#ffffff, #381e72);  --mat-sys-surface: light-dark(#fffbfe, #1c1b1f);  --mat-sys-on-surface: light-dark(#1c1b1f, #e6e1e5);  /* ... 80+ more design tokens ... */}

The light-dark() CSS function picks between two values based on the active color-scheme. When the user's OS is in dark mode (and your app sets color-scheme: light dark), the browser automatically selects the dark value. No JavaScript required. No bundle swap. One stylesheet, both modes.

This is the architectural shift: M2 baked colors into selector rules at build time; M3 declares intent via variables and lets the browser resolve them at runtime.

How components consume the tokens

Every Material component's styles reference --mat-sys-* variables:

css
/* Inside MatButton's compiled styles — pseudo-code */.mat-mdc-button {  color: var(--mdc-text-button-label-text-color, var(--mat-sys-primary));  background: transparent;}
.mat-mdc-filled-button {  background: var(--mdc-filled-button-container-color, var(--mat-sys-primary));  color: var(--mdc-filled-button-label-text-color, var(--mat-sys-on-primary));}

When you override --mat-sys-primary in your theme, every component that reads that token updates automatically — no component-specific overrides needed.

Setup

bash
ng add @angular/material

The schematic:

  • Installs @angular/material and @angular/cdk
  • Adds provideAnimationsAsync() to your providers
  • Adds the base theme to styles.scss
  • Adds Roboto font and Material Icons to index.html
typescript
// app.config.tsimport { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
export const appConfig: ApplicationConfig = {  providers: [    provideRouter(routes),    provideAnimationsAsync(),   // lazy-loads animation module (recommended)    // provideAnimations()      // eager — use if you need animations on first render  ],};
scss
// styles.scss@use '@angular/material' as mat;
html {  color-scheme: light dark;   // enables light-dark() CSS function  @include mat.theme((    color: mat.$violet-palette,    typography: Roboto,    density: 0,  ));}
body {  background: var(--mat-sys-surface);  color: var(--mat-sys-on-surface);  margin: 0;  font-family: Roboto, sans-serif;}

Using Material components

Import only the components your component uses. Angular tree-shakes the rest:

typescript
import { Component, inject } from '@angular/core';import { MatButtonModule } from '@angular/material/button';import { MatInputModule } from '@angular/material/input';import { MatFormFieldModule } from '@angular/material/form-field';import { MatIconModule } from '@angular/material/icon';import { MatCardModule } from '@angular/material/card';import { ReactiveFormsModule, FormControl } from '@angular/forms';
@Component({  selector: 'app-login',  standalone: true,  imports: [    ReactiveFormsModule,    MatCardModule,    MatFormFieldModule,    MatInputModule,    MatButtonModule,    MatIconModule,  ],  template: `    <mat-card>      <mat-card-header>        <mat-card-title>Sign In</mat-card-title>      </mat-card-header>
      <mat-card-content>        <mat-form-field appearance="outline">          <mat-label>Email</mat-label>          <input matInput type="email" [formControl]="email" />          <mat-icon matSuffix>email</mat-icon>          @if (email.hasError('required')) {            <mat-error>Email is required</mat-error>          }        </mat-form-field>
        <mat-form-field appearance="outline">          <mat-label>Password</mat-label>          <input matInput type="password" [formControl]="password" />        </mat-form-field>      </mat-card-content>
      <mat-card-actions>        <button mat-flat-button color="primary" (click)="login()">          Log In        </button>        <button mat-button>Forgot password?</button>      </mat-card-actions>    </mat-card>  `,})export class LoginComponent {  email = new FormControl('');  password = new FormControl('');  login() { /* ... */ }}
html
<!-- Buttons --><button mat-button>Text</button><button mat-stroked-button>Outlined</button><button mat-flat-button>Filled</button><button mat-icon-button><mat-icon>favorite</mat-icon></button><button mat-fab><mat-icon>add</mat-icon></button>
<!-- Form fields --><mat-form-field appearance="outline">  <mat-label>Name</mat-label>  <input matInput placeholder="Enter name" />  <mat-hint>Shown below the field</mat-hint></mat-form-field>
<!-- Select --><mat-form-field>  <mat-label>Country</mat-label>  <mat-select>    <mat-option value="us">United States</mat-option>    <mat-option value="fr">France</mat-option>  </mat-select></mat-form-field>
<!-- Progress indicators --><mat-spinner /><mat-progress-bar mode="indeterminate" /><mat-progress-bar mode="determinate" [value]="progress()" />
<!-- Navigation --><mat-toolbar color="primary">  <span>App Name</span>  <span class="spacer"></span>  <button mat-icon-button><mat-icon>menu</mat-icon></button></mat-toolbar>
<!-- Chips --><mat-chip>Angular</mat-chip><mat-chip-set>  @for (tag of tags(); track tag) {    <mat-chip (removed)="removeTag(tag)">      {{ tag }}      <mat-icon matChipRemove>cancel</mat-icon>    </mat-chip>  }</mat-chip-set>
<!-- Snackbar — programmatic --><!-- private snackBar = inject(MatSnackBar); --><!-- this.snackBar.open('Saved!', 'Dismiss', { duration: 3000 }); -->

Theming in depth

Available palettes

scss
// Pre-built Material Design 3 palettes (choose one as your primary)mat.$red-palettemat.$pink-palettemat.$purple-palettemat.$deep-purple-palettemat.$indigo-palettemat.$blue-palettemat.$light-blue-palettemat.$cyan-palettemat.$teal-palettemat.$green-palettemat.$light-green-palettemat.$lime-palettemat.$yellow-palettemat.$amber-palettemat.$orange-palettemat.$deep-orange-palettemat.$brown-palettemat.$grey-palettemat.$blue-grey-palettemat.$violet-palette  // default for new projectsmat.$rose-palette

Custom color palette from your brand color

Use the Material Design 3 color system to generate a full palette from a single seed color at material.io/color:

scss
// styles.scss — custom palette from brand color@use '@angular/material' as mat;
// Use hex value of your brand's primary color$my-palette: mat.define-palette((  0:   #000000,  10:  #21005d,  20:  #381e72,  25:  #432b7a,  30:  #4f3884,  35:  #5c458e,  40:  #6750a4,  50:  #7f67be,  60:  #9a80d9,  70:  #b69af5,  80:  #d0bcff,  90:  #eaddff,  95:  #f6edff,  98:  #fdf7ff,  99:  #fffbfe,  100: #ffffff,  secondary: (/* similar tonal palette */),  neutral: (/* grayscale tones */),  neutral-variant: (/* slightly tinted neutral */),  error: (/* red error palette */),));
html {  color-scheme: light dark;  @include mat.theme((    color: $my-palette,    typography: 'Inter',    density: 0,  ));}

For most projects, using the Material Theme Builder at material.io/theme-builder to generate your palette and download a SCSS file is faster than hand-crafting tonal values.

Dark mode strategies

scss
// Strategy 1 — System preference (recommended)// light-dark() automatically reads prefers-color-schemehtml {  color-scheme: light dark;  @include mat.theme(( color: mat.$violet-palette, ... ));}
// Strategy 2 — Explicit class toggle (user preference stored in localStorage)// Toggled by JavaScript: document.documentElement.classList.toggle('dark-theme')html {  color-scheme: light;  // default light  @include mat.theme(( color: mat.$violet-palette, ... ));}
html.dark-theme {  color-scheme: dark;}
// Strategy 3 — Explicit theme-type (no light-dark() function)@include mat.theme((  color: (    theme-type: dark,     // light | dark | color-scheme    palette: mat.$violet-palette,  ),));

Overriding design tokens

scss
// Override a system token globallyhtml {  @include mat.theme(( color: mat.$violet-palette ));
  // Override a specific token after the theme  --mat-sys-primary: #005cbb;          // brand override  --mat-sys-on-primary: #ffffff;}
// Override a specific component's tokens in a scope.high-emphasis-area {  @include mat.button-overrides((    filled-container-color: var(--mat-sys-error),    filled-label-text-color: var(--mat-sys-on-error),  ));}
// Override a token for one component instance with :host// (inside a component's SCSS file):host {  --mat-form-field-outline-color: var(--mat-sys-primary);  --mat-form-field-label-text-color: var(--mat-sys-primary);}

Dialogs — programmatic overlay components

Some Material components are opened programmatically rather than used in templates:

typescript
import { Component, inject, signal } from '@angular/core';import { MatDialog } from '@angular/material/dialog';import { MatSnackBar } from '@angular/material/snack-bar';import { ConfirmDialogComponent } from './confirm-dialog.component';
@Component({ /* ... */ })export class ProductPageComponent {  private dialog = inject(MatDialog);  private snackBar = inject(MatSnackBar);
  async confirmDelete(productId: string): Promise<void> {    const ref = this.dialog.open(ConfirmDialogComponent, {      width: '400px',      data: { title: 'Delete product?', message: 'This cannot be undone.' },    });
    const confirmed = await ref.afterClosed().toPromise();
    if (confirmed) {      await this.productService.delete(productId);      this.snackBar.open('Product deleted', 'Dismiss', { duration: 3000 });    }  }}
typescript
// confirm-dialog.component.tsimport { Component, inject } from '@angular/core';import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';import { MatButtonModule } from '@angular/material/button';
@Component({  standalone: true,  imports: [MatDialogModule, MatButtonModule],  template: `    <h2 mat-dialog-title>{{ data.title }}</h2>    <mat-dialog-content>{{ data.message }}</mat-dialog-content>    <mat-dialog-actions align="end">      <button mat-button mat-dialog-close>Cancel</button>      <button mat-flat-button [mat-dialog-close]="true" color="warn">        Delete      </button>    </mat-dialog-actions>  `,})export class ConfirmDialogComponent {  data = inject(MAT_DIALOG_DATA);  dialogRef = inject(MatDialogRef);}

Common mistakes

Mistake 1 — Applying old M2 theming API to M3

The old mat.define-light-theme() / mat.all-component-themes() pattern is the M2 API. If your styles.scss uses it, you're running M2 components with M2 theming. For v22 new projects, use mat.theme():

scss
// ❌ M2 API — still works but the old design system; not M3@include mat.core();$theme: mat.define-light-theme(( color: ( primary: mat.define-palette(mat.$indigo-palette) ) ));@include mat.all-component-themes($theme);
// ✅ M3 API (v19+) — Material Design 3 with CSS custom propertieshtml {  color-scheme: light dark;  @include mat.theme(( color: mat.$violet-palette, typography: Roboto ));}

Mistake 2 — Forgetting provideAnimations in providers

Many Material components use Angular animations (dialogs opening, snackbars sliding in, etc.). Without an animations provider, these silently don't animate (dialogs still open but without transition):

typescript
// ❌ No animation provider — dialogs open instantly, no transitionsexport const appConfig: ApplicationConfig = {  providers: [provideRouter(routes)],};
// ✅ Add the animations provider (async is recommended)import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
export const appConfig: ApplicationConfig = {  providers: [    provideRouter(routes),    provideAnimationsAsync(),  ],};

Mistake 3 — Using ::ng-deep to override Material internals

With M3, CSS custom properties are the intended override mechanism. ::ng-deep works but creates global leaking styles. Check for a --mat-* or --mdc-* token first:

scss
// ❌ ng-deep — global, fragile, breaks with internal DOM refactors:host ::ng-deep .mat-mdc-form-field-outline { border-color: red; }
// ✅ Check material.angular.dev for exposed tokens:host {  --mat-form-field-outlined-hover-border-color: red;  --mat-form-field-outlined-focus-border-color: red;}

Inspect the component's available tokens at material.angular.dev under each component's "Theming" tab.

Mistake 4 — Not using NoopAnimationsModule in tests

Component tests that include Material components need animations disabled, otherwise async animation sequences can cause test flakiness:

typescript
// ❌ Real animations in tests — inconsistent timing, test failuresTestBed.configureTestingModule({  imports: [MyComponent],  providers: [provideAnimations()],});
// ✅ Noop animations in testsimport { provideNoopAnimations } from '@angular/platform-browser/animations';
TestBed.configureTestingModule({  imports: [MyComponent],  providers: [provideNoopAnimations()],});

Mistake 5 — Importing MatXxxModule when individual directives suffice

For standalone components, you can import the specific directive/component class instead of the entire module. Modules include extra utilities that may not tree-shake:

typescript
// Works but imports the full module (including things you may not need)imports: [MatButtonModule]
// More precise — imports only the MatButton directive (Angular 15+)import { MatButton } from '@angular/material/button';imports: [MatButton]

In practice, both tree-shake well with esbuild. Prefer whichever makes your imports array cleaner and more self-documenting.

How this evolved

  • Angular Material Beta (2016): The library launched alongside Angular 2. Heavily NgModule-based. Material Design 1 styling. No theming system — just pre-built color classes. Required BrowserAnimationsModule in the root NgModule.

  • Angular Material v6–12 (2018–2021): Material Design 2. Sass-based theming with mat.define-palette(), mat.define-light-theme(), mat.all-component-themes(). The generated static CSS was comprehensive but all hardcoded values — no dynamic theming at runtime.

  • Angular Material v14 (2022): MDC migration completed — all components migrated to MDC (Material Design Components for Web) as the underlying implementation. Many ::ng-deep overrides from M1 broke. CSS class names changed from .mat-button to .mat-mdc-button.

  • Angular Material v17–18 (2023–2024): Material Design 3 shipped. Initial M3 theming via mat.define-theme() (experimental). CSS custom properties introduced as the theming primitive. @angular/material standalone components fully supported.

  • Angular Material v19 (2024): The mat.theme() mixin — simplified M3 API — stabilized. Replaces mat.define-theme(), mat.define-light-theme(), and mat.all-component-themes(). Dark mode via light-dark() CSS function enabled by default in new projects.

  • Angular Material v22 (now): mat.theme() is the standard. All components expose CSS custom properties as their styling API. The old M2 API still works but references v18.material.angular.dev for docs. New projects: M3 design tokens, mat.theme(), provideAnimationsAsync().

See also

0%