Angular 90 ๐ ฐ๏ธ Angular Material โ Theming
Angular Material’s theming system was completely rewritten in version 19. The old approach โ defining palettes, composing them into a theme map, and passing that map to component mixins โ was replaced with a design token system based on Material Design 3 (M3) . The new API generates CSS variables from a single theme configuration and gives you native CSS mechanisms for switching between light and dark mode without duplicating component styles .
The transition is significant. Code written for Angular Material 18 and earlier does not work with the v19+ theming API without modification. The old mat.define-light-theme, mat.define-dark-theme, and mat.all-component-themes functions were replaced with mat.theme, mat.system-classes, and per-component override mixins . The new system is more flexible, more aligned with the M3 specification, and more predictable once you understand the token model.
Key point: The new theming system is built on system tokens โ CSS variables prefixed with --mat-sys- that define every color, typography size, and shape in the design system. The mat.theme mixin generates these tokens from a configuration. Components read from the tokens. To customize a component, you override the tokens for that component or globally. To switch between light and dark mode, you change the color-scheme CSS property, and the light-dark() CSS function selects the appropriate token values automatically .
Why the theming system changed
The old theming system had structural limitations that became apparent as Material Design 3 matured. The rewrite addresses them.
The duplication problem. In the old system, applying a dark theme required re-running the component theme mixins with the dark theme map. This generated duplicate CSS for every component โ the light styles and the dark styles โ even though only the colors changed . The new system generates the color tokens once, and the light-dark() function switches between the light and dark values natively. There is no duplication.
The customization problem. In the old system, customizing a single component required extracting the component’s mixin, understanding the theme map structure, and passing the right palette keys. The new system exposes system tokens directly. A custom component reads var(--mat-sys-primary) instead of extracting the primary palette from a Sass map . This is simpler and works without Sass.
The token problem. Material Design 3 is built on design tokens โ named values for every color, typography scale, and shape in the system. The old theming system did not expose these tokens. The new system does. Every M3 token is available as a CSS variable, and the mat.theme mixin generates them from a configuration .
The native CSS problem. The light-dark() CSS function and the color-scheme property are now supported in all modern browsers. They allow a single set of CSS rules to produce both light and dark mode output based on the browser’s preference. The old theming system predated this capability. The new system uses it .
The trade-off. The new system is not backward-compatible. Applications upgrading from Angular Material 18 to 19 must migrate their theme files. The migration is mechanical โ replace mat.define-light-theme with mat.theme, replace component mixins with token overrides โ but it is not automatic. The Angular Material migration guide provides the step-by-step instructions.
a. The mat.theme Mixin and Design Tokens
The mat.theme mixin is the entry point for theming. It accepts a configuration object with three optional keys: color, typography, and density. The mixin generates the system tokens and applies them to the html element .
// styles.scss
@use '@angular/material' as mat;
html {
color-scheme: light dark;
@include mat.theme((
color: (
primary: mat.$azure-palette,
tertiary: mat.$blue-palette,
),
typography: Roboto,
density: 0,
));
}
The color key accepts either a single palette or a map with primary, tertiary, and theme-type keys. When theme-type is omitted and color-scheme: light dark is set, the mixin generates both light and dark token values using the light-dark() function . The typography key accepts a font family name or a map with plain-family, brand-family, and weight values. The density key accepts a number where 0 is standard and negative values are more compact .
The generated tokens are CSS variables. The full set is prefixed with --mat-sys-. The color tokens include --mat-sys-primary, --mat-sys-on-primary, --mat-sys-surface, --mat-sys-on-surface, --mat-sys-error, and their container and variant forms . The typography tokens include --mat-sys-body-medium, --mat-sys-headline-large, and the rest of the M3 scale. The shape tokens include --mat-sys-corner-small, --mat-sys-corner-medium, and the elevation tokens.
To apply the surface colors to your application, you set the background and text color on the body:
body {
background: var(--mat-sys-surface);
color: var(--mat-sys-on-surface);
}
Alternatively, the mat.system-classes() mixin generates utility classes that apply the tokens. The mat-bg-surface, mat-text-on-surface, and mat-font-body-medium classes can be applied directly in templates .
html {
@include mat.theme((/* ... */));
@include mat.system-classes();
}
<body class="mat-bg-surface mat-text-on-surface">
b. Color Palettes and Custom Palettes
Angular Material provides twelve prebuilt color palettes that can be used with mat.theme. The palettes are available as Sass variables: $red-palette, $green-palette, $blue-palette, $yellow-palette, $cyan-palette, $magenta-palette, $orange-palette, $chartreuse-palette, $spring-green-palette, $azure-palette, $violet-palette, and $rose-palette .
A single palette can be provided for the primary color, and the mixin generates the secondary and tertiary colors from it. Alternatively, a map can specify different palettes for primary and tertiary:
@include mat.theme((
color: (
primary: mat.$violet-palette,
tertiary: mat.$orange-palette,
),
));
For custom colors, the Angular CLI schematic ng generate @angular/material:theme-color generates a custom palette file from a single color input. The schematic prompts for the primary color and optionally the secondary, tertiary, and neutral colors, and produces a Sass file with the generated palettes .
ng generate @angular/material:theme-color
The generated file contains the full palette definition with all the tones that M3 requires. The palette is then imported and used in the mat.theme configuration.
c. Light and Dark Mode
The new theming system handles light and dark mode through the color-scheme CSS property and the light-dark() CSS function. When color-scheme: light dark is set on the html element, the browser selects the appropriate color values based on the user’s system preference .
html {
color-scheme: light dark;
@include mat.theme((
color: mat.$violet-palette,
));
}
The light-dark() function is used inside the generated tokens. Each token has a light value and a dark value, and the browser selects the correct one based on the computed color-scheme .
To force a specific mode regardless of system preference, set color-scheme: light or color-scheme: dark on the html element. To allow the user to toggle between modes, the application can set the color-scheme property dynamically based on a stored preference .
// theme.service.ts
@Injectable({ providedIn: 'root' })
export class ThemeService {
private document = inject(DOCUMENT);
setTheme(theme: 'light' | 'dark'): void {
this.document.documentElement.style.setProperty('color-scheme', theme);
}
initializeTheme(): void {
const saved = localStorage.getItem('theme');
if (saved === 'light' || saved === 'dark') {
this.setTheme(saved);
}
}
}
The service sets the color-scheme property directly on the html element. The browser then switches the token values automatically. No CSS classes are needed, and no duplicate component styles are generated .
For applications that need to apply a theme to a specific subtree rather than globally, the mat.theme mixin can be included under a CSS selector. The tokens are scoped to that selector, and the components inside it use the scoped values.
Complete Example Session
This session builds a custom theme with a generated palette, a dark mode toggle, and a custom component that reads from the system tokens.
// ============================================
// PART 1: THE THEME FILE
// ============================================
// styles.scss
@use '@angular/material' as mat;
@use './theme-colors' as my-colors;
html {
color-scheme: light dark;
@include mat.theme((
color: (
primary: my-colors.$primary-palette,
tertiary: mat.$blue-palette,
),
typography: (
plain-family: 'Open Sans',
brand-family: 'Josefin Sans',
bold-weight: 700,
medium-weight: 500,
regular-weight: 300,
),
density: 0,
));
@include mat.system-classes();
}
body {
background: var(--mat-sys-surface);
color: var(--mat-sys-on-surface);
margin: 0;
font-family: var(--mat-sys-body-medium);
}
The mat.theme mixin generates the system tokens. The mat.system-classes() mixin generates the utility classes. The body uses the surface tokens for background and text color .
// ============================================
// PART 2: THE GENERATED PALETTE
// ============================================
// theme-colors.scss (generated by ng generate @angular/material:theme-color)
@use 'sass:map';
@use '@angular/material' as mat;
$_palettes: (
primary: (
0: #000000,
10: #21005d,
20: #381e72,
// ... all tones
),
secondary: (/* ... */),
tertiary: (/* ... */),
neutral: (/* ... */),
neutral-variant: (/* ... */),
error: (/* ... */),
);
$_rest: (
secondary: map.get($_palettes, secondary),
neutral: map.get($_palettes, neutral),
neutral-variant: map.get($_palettes, neutral-variant),
error: map.get($_palettes, error),
);
$primary-palette: map.merge(map.get($_palettes, primary), $_rest);
$tertiary-palette: map.merge(map.get($_palettes, tertiary), $_rest);
The generated palette file contains the full M3 palette definition with all tones. The $_rest map merges the secondary, neutral, neutral-variant, and error palettes into the primary and tertiary palettes so that each is a complete palette .
// ============================================
// PART 3: THE THEME TOGGLE SERVICE
// ============================================
// theme.service.ts
import { Injectable, inject, DOCUMENT } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ThemeService {
private document = inject(DOCUMENT);
private readonly STORAGE_KEY = 'app-theme';
initialize(): void {
const saved = localStorage.getItem(this.STORAGE_KEY);
if (saved === 'light' || saved === 'dark') {
this.setTheme(saved);
return;
}
// Follow system preference if no saved theme
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
this.setTheme(prefersDark ? 'dark' : 'light');
}
setTheme(theme: 'light' | 'dark'): void {
this.document.documentElement.style.setProperty('color-scheme', theme);
localStorage.setItem(this.STORAGE_KEY, theme);
}
toggle(): 'light' | 'dark' {
const current = this.document.documentElement.style.getPropertyValue('color-scheme') || 'light';
const next = current === 'dark' ? 'light' : 'dark';
this.setTheme(next);
return next;
}
}
The service sets the color-scheme property on the html element. The browser switches the token values automatically. No CSS classes are needed, and no component styles are duplicated .
// ============================================
// PART 4: THE THEME TOGGLE BUTTON
// ============================================
// theme-toggle.component.ts
import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { ThemeService } from './theme.service';
@Component({
selector: 'app-theme-toggle',
imports: [MatButtonModule, MatIconModule],
template: `
<button mat-icon-button (click)="toggleTheme()" aria-label="Toggle theme">
<mat-icon>{{ isDark() ? 'light_mode' : 'dark_mode' }}</mat-icon>
</button>
`,
})
export class ThemeToggleComponent {
private themeService = inject(ThemeService);
isDark(): boolean {
return document.documentElement.style.getPropertyValue('color-scheme') === 'dark';
}
toggleTheme(): void {
this.themeService.toggle();
}
}
The button toggles the color-scheme property. The icon changes based on the current mode. The theme switch is instantaneous and does not require a page reload .
// ============================================
// PART 5: THE CUSTOM COMPONENT WITH TOKENS
// ============================================
// custom-card.component.scss
.custom-card {
background: var(--mat-sys-surface-container);
color: var(--mat-sys-on-surface);
border-radius: var(--mat-sys-corner-medium);
padding: 16px;
box-shadow: var(--mat-sys-level1);
}
.custom-card__title {
font: var(--mat-sys-title-large);
color: var(--mat-sys-primary);
}
.custom-card__body {
font: var(--mat-sys-body-medium);
color: var(--mat-sys-on-surface-variant);
}
.custom-card__error {
color: var(--mat-sys-error);
}
The custom component reads from the system tokens directly. It does not need to extract values from a Sass theme map. The tokens are CSS variables, so they work in plain CSS and in component styles .
// ============================================
// PART 6: THE COMPONENT CLASS
// ============================================
// custom-card.component.ts
import { Component, input } from '@angular/core';
@Component({
selector: 'app-custom-card',
template: `
<div class="custom-card">
<h3 class="custom-card__title">{{ title() }}</h3>
<p class="custom-card__body">{{ body() }}</p>
@if (hasError()) {
<p class="custom-card__error">{{ errorMessage() }}</p>
}
</div>
`,
styleUrl: './custom-card.component.scss',
})
export class CustomCardComponent {
title = input.required<string>();
body = input.required<string>();
hasError = input(false);
errorMessage = input('');
}
The component uses the token-based styles. The card background, title, body, and error colors all come from the M3 system tokens. When the user switches themes, the card updates automatically .
// ============================================
// PART 7: THE COMPONENT OVERRIDE
// ============================================
// styles.scss
@use '@angular/material' as mat;
// Override the button's corner radius globally
html {
@include mat.theme((/* ... */));
@include mat.button-overrides((
filled-container-shape: 4px,
outlined-container-shape: 4px,
));
}
The mat.button-overrides mixin accepts a map of component token overrides. The filled-container-shape and outlined-container-shape tokens control the corner radius of the buttons. The override is applied globally, and only the button styles are affected .
// ============================================
// PART 8: THE COMPONENT-SPECIFIC THEME
// ============================================
// Only apply a custom shape to buttons inside the .compact-buttons container
.compact-buttons {
@include mat.button-overrides((
filled-container-shape: 2px,
outlined-container-shape: 2px,
));
}
The override can be scoped to a CSS selector. Buttons inside .compact-buttons get the custom shape, while buttons elsewhere use the default. This is how per-component customization works without affecting the rest of the application .
// ============================================
// PART 9: THE TYPOGRAPHY CONFIGURATION
// ============================================
// styles.scss
@use '@angular/material' as mat;
html {
@include mat.theme((
typography: (
plain-family: 'Inter',
brand-family: 'Playfair Display',
bold-weight: 800,
medium-weight: 600,
regular-weight: 400,
),
));
}
The typography configuration accepts a single font family or a map. The plain-family is used for body text. The brand-family is used for headings and titles. The weight values control the bold, medium, and regular text .
// ============================================
// PART 10: THE DENSITY CONFIGURATION
// ============================================
// styles.scss
@use '@angular/material' as mat;
html {
@include mat.theme((
density: -1, // More compact than default
));
}
// Or apply density to a specific component
.compact-form {
@include mat.form-field-density(-3);
}
The density scale ranges from 0 (default) to -5 (most compact). Negative values reduce the vertical spacing in components. The mat.form-field-density mixin applies density to a specific component without affecting the global theme .
Quick Reference
The mat.theme Configuration
| Key | Type | Example |
|---|---|---|
color | Palette or map | mat.$azure-palette or { primary, tertiary, theme-type } |
typography | String or map | 'Roboto' or { plain-family, brand-family, bold-weight } |
density | Number | 0 (default), -1 (compact), -3 (dense) |
The Prebuilt Palettes
| Palette | Variable |
|---|---|
| Red | mat.$red-palette |
| Green | mat.$green-palette |
| Blue | mat.$blue-palette |
| Yellow | mat.$yellow-palette |
| Cyan | mat.$cyan-palette |
| Magenta | mat.$magenta-palette |
| Orange | mat.$orange-palette |
| Chartreuse | mat.$chartreuse-palette |
| Spring Green | mat.$spring-green-palette |
| Azure | mat.$azure-palette |
| Violet | mat.$violet-palette |
| Rose | mat.$rose-palette |
The System Token Categories
| Category | Example Tokens |
|---|---|
| Primary | --mat-sys-primary, --mat-sys-on-primary, --mat-sys-primary-container |
| Surface | --mat-sys-surface, --mat-sys-on-surface, --mat-sys-surface-container |
| Error | --mat-sys-error, --mat-sys-on-error, --mat-sys-error-container |
| Typography | --mat-sys-body-medium, --mat-sys-headline-large, --mat-sys-title-small |
| Shape | --mat-sys-corner-small, --mat-sys-corner-medium, --mat-sys-corner-large |
| Elevation | --mat-sys-level0 through --mat-sys-level5 |
The Light/Dark Mode Configuration
| Configuration | Effect |
|---|---|
color-scheme: light dark | Follows system preference |
color-scheme: light | Forces light mode |
color-scheme: dark | Forces dark mode |
light-dark() function | Selects token value based on color-scheme |
The Component Override Mixins
| Mixin | Component |
|---|---|
mat.button-overrides | Buttons |
mat.form-field-overrides | Form fields |
mat.dialog-overrides | Dialogs |
mat.card-overrides | Cards |
mat.table-overrides | Tables |
Best Practices
โ Do This:
// Use mat.theme with color-scheme: light dark
html {
color-scheme: light dark;
@include mat.theme((color: mat.$azure-palette));
} // โ
// Use system tokens for custom components
.custom { background: var(--mat-sys-surface); } // โ
// Use component override mixins for targeted changes
@include mat.button-overrides((filled-container-shape: 4px)); // โ
// Set color-scheme on the document element for theme switching
document.documentElement.style.setProperty('color-scheme', theme); // โ
// Use mat.system-classes for utility classes
@include mat.system-classes(); // โ
โ Don’t Do This:
// Don't use the old M2 theming API
@include mat.define-light-theme(...) // โ removed in v19
// Don't duplicate component themes for dark mode
.dark-theme { @include mat.all-component-themes($dark); } // โ duplicates CSS
// Don't override component styles with deep selectors
::ng-deep .mat-mdc-button { ... } // โ
// Don't use @import in SCSS
@import '@angular/material'; // โ use @use
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Components unstyled | mat.theme not included | Add to styles.scss |
| Dark mode not working | color-scheme not set | Add color-scheme: light dark |
| Duplicate CSS | Using old theme mixins | Use mat.theme and tokens |
| Tokens undefined | mat.theme not applied to html | Include on html or a parent |
| Custom palette not applied | Wrong palette variable | Check the generated file |
| Theme toggle requires reload | Using classes instead of color-scheme | Set color-scheme directly |
Real-World Examples
1. Basic Theme
html { @include mat.theme((color: mat.$violet-palette)); }
2. Custom Palette Theme
@include mat.theme((color: (primary: my-colors.$primary-palette)));
3. Typography Configuration
@include mat.theme((typography: (plain-family: 'Inter')));
4. Density Configuration
@include mat.theme((density: -1));
5. Light/Dark Mode
html { color-scheme: light dark; }
6. Force Dark Mode
document.documentElement.style.setProperty('color-scheme', 'dark');
7. System Tokens
.custom { background: var(--mat-sys-surface); }
8. Utility Classes
<body class="mat-bg-surface mat-text-on-surface">
9. Component Override
@include mat.button-overrides((filled-container-shape: 4px));
10. Scoped Density
.compact { @include mat.form-field-density(-3); }
Visual
The Theming Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ mat.theme CONFIGURATION โ
โ color, typography, density โ
โ โ โ
โ โผ โ
โ SYSTEM TOKENS (CSS variables) โ
โ --mat-sys-primary โ
โ --mat-sys-surface โ
โ --mat-sys-body-medium โ
โ โ โ
โ โโโ> Angular Material components โ
โ โโโ> Custom components โ
โ โโโ> Utility classes โ
โ โ
โ color-scheme: light dark โ
โ โโ light-dark() selects token values โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Light/Dark Mode Flow
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ color-scheme: light dark โ
โ โ โ
โ โผ โ
โ System preference: dark โ
โ โ โ
โ โผ โ
โ Browser selects dark token values โ
โ --mat-sys-surface: #1a1a1a โ
โ --mat-sys-on-surface: #ffffff โ
โ โ โ
โ โผ โ
โ Components render with dark colors โ
โ โ
โ No CSS duplication. No page reload. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Token Categories
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ --mat-sys-* TOKENS โ
โ โ
โ Color: โ
โ primary, on-primary, primary-container โ
โ surface, on-surface, surface-container โ
โ error, on-error, error-container โ
โ โ
โ Typography: โ
โ body-medium, headline-large โ
โ title-small, label-large โ
โ โ
โ Shape: โ
โ corner-small, corner-medium โ
โ corner-large, corner-extra-large โ
โ โ
โ Elevation: โ
โ level0 through level5 โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Theme Toggle
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THEME TOGGLE โ
โ โ
โ ThemeService.setTheme('dark') โ
โ โ โ
โ โผ โ
โ document.documentElement.style โ
โ .setProperty('color-scheme', 'dark') โ
โ โ โ
โ โผ โ
โ Browser switches all light-dark() values โ
โ โ โ
โ โผ โ
โ All components update instantly โ
โ โ
โ No CSS classes. No duplicate styles. โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Theming API | mat.theme mixin |
| Design system | Material Design 3 (M3) |
| Token prefix | --mat-sys- |
| Light/dark mode | color-scheme + light-dark() |
| Prebuilt palettes | 12 (red, green, blue, violet, etc.) |
| Custom palette | ng generate @angular/material:theme-color |
| Typography config | plain-family, brand-family, weights |
| Density config | 0 to -5 |
| Utility classes | mat.system-classes() |
| Component overrides | mat.button-overrides, mat.form-field-overrides |
| Old API | Removed in v19 (M2) |
Key takeaways:
- Angular Material 19 replaced the M2 theming system with a design token system based on Material Design 3. The old
mat.define-light-themeandmat.all-component-themesfunctions are gone. The new API ismat.theme, which generates system tokens from a configuration . - The
mat.thememixin accepts color, typography, and density configuration. The color can be a single palette or a map withprimary,tertiary, andtheme-type. The typography can be a font family name or a map with plain and brand families. The density is a number from 0 to -5 . - System tokens are CSS variables prefixed with
--mat-sys-. They define every color, typography scale, and shape in the design system. Components read from the tokens. Custom components use the same tokens withvar(--mat-sys-primary). - Light and dark mode are handled by
color-schemeandlight-dark(). Settingcolor-scheme: light darkon thehtmlelement makes the browser select the appropriate token values based on system preference. Settingcolor-scheme: lightorcolor-scheme: darkforces a specific mode . - Theme switching does not require duplicate CSS. The
light-dark()function is applied inside the tokens. When thecolor-schemeproperty changes, the browser switches all token values automatically. There is no need to re-run component theme mixins . - Custom palettes are generated with the Angular CLI. The
ng generate @angular/material:theme-colorschematic creates a full M3 palette file from a single color input. The generated file contains all the tones that M3 requires . - Component-specific customization uses override mixins. The
mat.button-overrides,mat.form-field-overrides, and similar mixins accept a map of token overrides. The overrides can be applied globally or scoped to a CSS selector .
Remember: The Angular Material theming system is now based on design tokens. The mat.theme mixin generates the tokens. The color-scheme property switches between light and dark. The tokens are CSS variables that any component can read. This is simpler than the old M2 system, more aligned with the M3 specification, and more predictable once you understand the token model. Use the prebuilt palettes or generate a custom one. Set color-scheme: light dark for automatic mode switching. Use system tokens for custom components. Override component tokens when you need to customize a specific component. The theming system is the visual foundation of the entire application.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!