I am trying to create an example Angular 19 app with Angular Material v3 having the following features: custom light and dark themes; toggle theme at runtime; use color variants for primary, warn and the like as with the old color directive (a proper way for doing this in V3 is welcome, I fear it will imply a lot more themes...). I thus created a new Angular 19 app and added Angular Material via ng add @angular/material. You can find a full repro repository at https://github.com/Myrmex/angular-theming. The relevant code is reported below: (1) For styles, I ensured that no prebuilt theme is included in styles in my angular.json file and wrote this styles.scss: // styles.scss @use "@angular/material" as mat; @include mat.core(); // create color configurations for light and dark themes $light-theme: mat.define-theme( ( color: ( theme-type: light, primary: mat.$blue-palette, ), ) ); $dark-theme: mat.define-theme( ( color: ( theme-type: dark, primary: mat.$blue-palette, ), ) ); // base styles html { color-scheme: light dark; } // default (light) theme :root, html:not(.dark-theme) { @include mat.all-component-themes($light-theme); & { color-scheme: light; } } // dark theme styles html.dark-theme { // @include mat.all-component-themes($dark-theme); @include mat.all-component-colors($dark-theme); & { color-scheme: dark; } } body { background: var(--mat-sys-surface); color: var(--mat-sys-on-surface); } @include mat.color-variants-backwards-compatibility($light-theme); @include mat.color-variants-backwards-compatibility($dark-theme); (2) I then added a theme service for toggling themes: // theme.service.ts import { Injectable, Inject, PLATFORM_ID } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; const THEME_KEY = 'app-theme-preference'; @Injectable({ providedIn: 'root', }) export class ThemeService { private readonly _htmlElement?: HTMLHtmlElement; constructor(@Inject(PLATFORM_ID) private platformId: Object) { // ensure we only access document/window in browser environment if (isPlatformBrowser(this.platformId)) { this._htmlElement = document.documentElement as HTMLHtmlElement; } } /** * Initialize theme on app startup */ public initializeTheme() { if (!isPlatformBrowser(this.platformId)) return; const savedTheme = this.getSavedTheme(); const systemTheme = this.getSystemTheme(); // Prefer saved theme, fall back to system theme const initialTheme = savedTheme || systemTheme; this.setTheme(initialTheme); // Listen for system theme changes const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); mediaQuery.addEventListener('change', (e) => { if (!this.getSavedTheme()) { this.setTheme(e.matches ? 'dark' : 'light'); } }); } /** * Toggle between light and dark themes */ public toggleTheme(): 'light' | 'dark' | null { if (!isPlatformBrowser(this.platformId)) return null; const currentTheme = this.getCurrentTheme(); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; this.setTheme(newTheme); return newTheme; } /** * Set a specific theme. * @param theme 'light' or 'dark'. */ public setTheme(theme: 'light' | 'dark') { if (!isPlatformBrowser(this.platformId)) return; console.log('Setting theme:', theme); // Save theme preference localStorage.setItem(THEME_KEY, theme); // Set color-scheme on html element this._htmlElement?.setAttribute('color-scheme', theme); // Toggle theme-specific classes if (theme === 'dark') { console.log('Adding dark-theme class'); this._htmlElement?.classList.add('dark-theme'); this._htmlElement?.classList.remove('light-theme'); } else { console.log('Adding light-theme class'); this._htmlElement?.classList.add('light-theme'); this._htmlElement?.classList.remove('dark-theme'); } } /** * Get the current active theme * @returns 'light' or 'dark'. */ public getCurrentTheme(): 'light' | 'dark' { if (!isPlatformBrowser(this.platformId)) return 'light'; return this._htmlElement?.getAttribute('color-scheme') as 'light' | 'dark'; } private getSystemTheme(): 'light' | 'dark' { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } /** * Get saved theme preference from localStorage * @returns Saved theme or null. */ private getSavedTheme(): 'light' | 'dark' | null { if (!isPlatformBrowser(this.platformId)) return null; const savedTheme = localStorage.getItem(THEME_KEY); return savedTheme === 'light' || savedTheme === 'dark' ? savedTheme : null; } /** * Set theme based on system preference */ public setThemeFromSystemPreference() { if (!isPlatformBrowser(this.platformId)) return; const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); const initialTheme = prefersDarkScheme.matches ? 'dark' : 'light'; this.setTheme(initialTheme); // Use addEventListener instead of deprecated addListener prefersDarkScheme.addEventListener('change', (e) => { // Only change if no saved preference exists if (!this.getSavedTheme()) { this.setTheme(e.matches ? 'dark' : 'light'); } }); } } (3) Finally, I added a toggler component: // theme-toggle.component.ts import { Component, OnInit } from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; import { ThemeService } from '../../services/theme.service'; @Component({ selector: 'app-theme-toggle', imports: [MatButtonModule, MatIconModule], templateUrl: './theme-toggle.component.html', styleUrl: './theme-toggle.component.scss', }) export class ThemeToggleComponent implements OnInit { isDarkMode = false; constructor(private themeService: ThemeService) {} public ngOnInit() { this.themeService.initializeTheme(); // set initial theme this.updateThemeMode(); } public toggleTheme() { this.themeService.toggleTheme(); this.updateThemeMode(); } private updateThemeMode() { this.isDarkMode = this.themeService.getCurrentTheme() === 'dark'; } } with its HTML template: <!-- theme-toggle.component.html --> <button type="button" mat-icon-button (click)="toggleTheme()" aria-label="Toggle theme" > <mat-icon>{{ isDarkMode ? "light_mode" : "dark_mode" }}</mat-icon> </button> This works, but I have some glitches: it seems that the default typography is not applied. Except when text is inside a button or other Material components, text appears with the default (Times New Roman) font and size, e.g. if I place a p element inside a mat-card. I tried to add typography via its simple font name, Roboto, by adding typography: Roboto as specified in the current theming guide; still, I get a compile error because here an object is expected, and I could find no example about this object. Further, I don't want to redefine typography, just ensure it gets applied. I need a quick and easy way to apply colors, so I added the compatibility include (see this question about color variants), also because I have many Angular libraries just using mat-primary and similar classes for those variants. Yet, this does not seem to work e.g. for button background colors: I get only a different foreground for the button's labels. As an example, here is my HTML in app.component.html: <article> <div> <app-theme-toggle /> </div> <hr /> <mat-card> <mat-card-header> <!-- this has the expected typography --> <mat-card-title>Custom Theming with Toggle</mat-card-title> </mat-card-header> <mat-card-content> <!-- this appears in Times... --> <p>This is an example.</p> <div class="form-row"> <!-- this has default background and only changes text foreground... --> <button type="button" mat-raised-button class="mat-primary"> primary </button> <!-- as above... --> <button type="button" mat-raised-button class="mat-warn"> warn </button> </div> </mat-card-content> </mat-card> </article> Continue reading...