Building a Runtime‑Switchable Custom Theme in Angular Material v14+
Learn how to create a custom Angular Material theme that can switch between light and dark at runtime using CSS variables, with a practical example and trade‑off analysis.
02 Aug 2026, 01:28 UTC

Problem: You need a theme that can change at runtime without a full page reload
Many Angular applications start with a single, static Material theme. When a user requests dark mode, the usual approach is to reload the app or maintain two separate builds, which hurts the user experience and adds maintenance overhead.
Thesis: Leverage Angular Material’s SCSS theming API and CSS variables (v14+) to define light and dark themes that can be swapped by toggling a class on the <body> element.
1. Define custom palettes
Angular Material’s theming relies on palette maps created with the mat-palette mixin. A palette consists of a hue (e.g., 500) and optional contrast hues.
// src/styles.scss
@use '@angular/material' as mat;
// Primary palette – indigo
$primary: mat.define-palette(mat.$indigo-palette, 500, 300, 700);
// Accent palette – pink
$accent: mat.define-palette(mat.$pink-palette, 500, 200, 800);
// Warn palette – red
$warn: mat.define-palette(mat.$red-palette, 500);
2. Assemble light and dark theme maps
Each theme map combines the three palettes and chooses a light or dark base. The mat-light-theme and mat-dark-theme mixins produce the theme object.
// Light theme
$light-theme: mat.define-light-theme((
color: (
primary: $primary,
accent: $accent,
warn: $warn
)
));
// Dark theme – same palettes, dark base
$dark-theme: mat.define-dark-theme((
color: (
primary: $primary,
accent: $accent,
warn: $warn
)
));
3. Include the themes and enable runtime switching
Angular Material v14+ emits CSS variables when the theme is included. We import both themes under separate class selectors so that only one set of variables is active at a time.
// src/styles.scss (continued)
@include mat.all-component-themes($light-theme);
dark-theme {
@include mat.all-component-themes($dark-theme);
}
Now toggling the dark-theme class on <body> swaps the active variables without a reload.
4. Worked example: toggling dark mode via a button
Add a simple service that toggles the class.
// src/app/theme-toggle.service.ts
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ThemeToggleService {
private isDark = false;
toggle() {
this.isDark = !this.isDark;
document.body.classList.toggle('dark-theme', this.isDark);
}
isDarkMode(): boolean {
return this.isDark;
}
}
Use it in a component:
{{ theme.isDarkMode() ? 'Light mode' : 'Dark mode' }}
// src/app/app.component.ts
import { Component } from '@angular/core';
import { ThemeToggleService } from './theme-toggle.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
})
export class AppComponent {
constructor(public theme: ThemeToggleService) {}
}
Trade‑offs and limitations
- Bundle size: Including both light and dark themes duplicates the generated CSS, increasing the payload. For most applications the increase is modest (< 50 KB gzipped), but if you plan to support many themes (e.g., multiple brand colors) consider lazy‑loading separate stylesheets or using the
@angular/materialtheming API withmat-coreto share common styles. - Browser support: CSS variables are not available in IE11. If you must support IE11, keep a static fallback theme (e.g., always load the light theme) or provide a separate stylesheet that does not rely on variables.
- Verification: After building (
ng build), inspect a Material component (e.g.,<mat-button>) in the browser’s DevTools. In the Computed tab you should see properties likebackground-color: var(--mat-primary-500). Toggling the dark class should change the variable values without a page reload.
Actionable closing
Start by adding Angular Material to a fresh CLI project (ng add @angular/material), define your palettes as shown, and include both light and dark themes under class selectors. Implement a simple toggle service to flip the dark-theme class on <body>. Verify the change via DevTools and monitor bundle size with ng build --stats-json. If the extra CSS becomes a concern, evaluate splitting themes into separate lazy‑loaded bundles or limiting the number of palettes you ship.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.