Angular Material
Google's official component library ships accessible Material Design 3 components with build-time theming through CSS custom properties
Modern Angular only No equivalent exists in the original 100 Days series. Written fresh for Angular v22, Angular Material v22, Material Design 3.
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:
| Layer | Package | What it provides |
|---|---|---|
| CDK | @angular/cdk | Platform primitives: overlays, drag-drop, virtual scroll, a11y, testing harnesses |
| Material components | @angular/material | 50+ Material Design 3 UI components built on the CDK |
| Material theming | SCSS mixins in @angular/material | Design 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:
// 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:
// 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:
/* 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:
/* 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
ng add @angular/materialThe schematic:
- Installs
@angular/materialand@angular/cdk - Adds
provideAnimationsAsync()to your providers - Adds the base theme to
styles.scss - Adds
Robotofont and Material Icons toindex.html
// 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 ],};// 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
Standalone imports (recommended)
Import only the components your component uses. Angular tree-shakes the rest:
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() { /* ... */ }}Common component gallery
<!-- 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
// 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-paletteCustom 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:
// 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
// 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
// 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:
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 }); } }}// 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():
// ❌ 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):
// ❌ 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:
// ❌ 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:
// ❌ 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:
// 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
BrowserAnimationsModulein 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-deepoverrides from M1 broke. CSS class names changed from.mat-buttonto.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/materialstandalone components fully supported. -
Angular Material v19 (2024): The
mat.theme()mixin — simplified M3 API — stabilized. Replacesmat.define-theme(),mat.define-light-theme(), andmat.all-component-themes(). Dark mode vialight-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 referencesv18.material.angular.devfor docs. New projects: M3 design tokens,mat.theme(),provideAnimationsAsync().
See also
- Component Harnesses — Material ships harnesses for every component; use them in tests instead of raw DOM selectors
- Sass & SCSS —
@use,:host, and CSS custom properties explained; prerequisite for understanding Material theming - View Encapsulation — why
:host { --mat-sys-primary: ... }correctly scopes token overrides to a component - Official Angular Material docs
- Material Theme Builder — generates custom palettes from your brand color
- Material Design 3 spec