Angular Material Dark Mode in 3 steps

A dark mode is a must-have little feature for your Angular web application. It is easier on the eyes and less draining for your smartphone battery.

With Angular Material 3, this is simpler than ever. You no longer need separate light and dark theme definitions, a .darkMode class, or special overlay hacks. Material now emits colors with the CSS light-dark() function, and you switch themes by setting the color-scheme property.

Here is how you can add light, dark, and system theme support to your Angular Material app in three simple steps.

Want the easiest path to a custom Material 3 theme with dark mode built in? Use Material Theme Builder to generate your colors, typography, and theme CSS, then drop it into your Angular app.

The end result will be a minimal Angular Material app with a theme menu for light, dark, and system preferences. Here is how it will look like.

Angular Material dark mode demo

Let's get started!

Step 1: Add a Material 3 theme with color-scheme

Angular Material 3 themes are created with the mat.theme() mixin. When you include it, Material generates system tokens like --mat-sys-primary and --mat-sys-background using the CSS light-dark() function.

Those tokens only switch between light and dark values when color-scheme is set. So the first step is to put both on html in your styles.scss:

@use "@angular/material" as mat;

html {
  color-scheme: light dark;

  @include mat.theme(
    (
      color: mat.$azure-palette,
      typography: Roboto,
      density: 0,
    )
  );
}

body {
  margin: 0;
  font-family: Roboto, "Helvetica Neue", sans-serif;
  background-color: var(--mat-sys-background);
  color: var(--mat-sys-on-background);
}

A few important bits:

  • color-scheme: light dark tells the browser to follow the user's system preference by default.
  • mat.theme() creates one theme that works for both light and dark, with no define-light-theme / define-dark-theme pair needed.
  • --mat-sys-* tokens are what you use for your own page styles, so custom UI follows the same theme as Material components.

I've just added some basic Material components so we can see the effect of the theme changes. Feel free to customize as you want (the code link is available at the end of this post). In light mode, it looks like this.

Angular Material light theme

With color-scheme: light dark, your app already respects the OS light/dark setting. Next, we'll let users override that choice.

Step 2: Add light, dark, and system switching

We need a way for users to choose light, dark, or system at runtime. A small theme service plus a menu in the toolbar keeps this clean.

First, create a ThemeService that stores the preference and applies it by setting color-scheme on the document:

import { Injectable, computed, effect, signal } from '@angular/core';

export type ThemeMode = 'light' | 'dark' | 'system';

export interface AppTheme {
  name: ThemeMode;
  icon: string;
}

const STORAGE_KEY = 'angular-dark-mode-theme';

@Injectable({
  providedIn: 'root',
})
export class ThemeService {
  private readonly appTheme = signal<ThemeMode>(this.readStoredTheme());

  private readonly themes: AppTheme[] = [
    { name: 'light', icon: 'light_mode' },
    { name: 'dark', icon: 'dark_mode' },
    { name: 'system', icon: 'desktop_windows' },
  ];

  readonly selectedTheme = computed(() =>
    this.themes.find((theme) => theme.name === this.appTheme())
  );

  constructor() {
    effect(() => {
      const theme = this.appTheme();
      const colorScheme = theme === 'system' ? 'light dark' : theme;

      document.documentElement.style.setProperty('color-scheme', colorScheme);
      localStorage.setItem(STORAGE_KEY, theme);
    });
  }

  getThemes(): AppTheme[] {
    return this.themes;
  }

  setTheme(theme: ThemeMode): void {
    this.appTheme.set(theme);
  }

  private readStoredTheme(): ThemeMode {
    const stored = localStorage.getItem(STORAGE_KEY);

    if (stored === 'light' || stored === 'dark' || stored === 'system') {
      return stored;
    }

    return 'system';
  }
}

The key line is setting color-scheme on document.documentElement (<html>):

  • light → force the light tokens
  • dark → force the dark tokens
  • light dark → follow the system preference

We also persist the choice in localStorage so it survives a refresh.

Then wire up a simple menu in the toolbar:

<mat-toolbar>
  Angular Dark Mode
  <div class="flex-stretch"></div>
  <button mat-icon-button [matMenuTriggerFor]="themeMenu">
    <mat-icon>{{ themeService.selectedTheme()?.icon }}</mat-icon>
  </button>
  <mat-menu #themeMenu="matMenu">
    @for (theme of themeService.getThemes(); track theme.name) {
      <button mat-menu-item (click)="themeService.setTheme(theme.name)">
        <mat-icon>{{ theme.icon }}</mat-icon>
        <span>{{ theme.name | titlecase }}</span>
      </button>
    }
  </mat-menu>
</mat-toolbar>

Great! If you test this now, the toolbar menu should switch the whole app between light, dark, and system. Material components and your --mat-sys-* page styles update together. Here is the same app in dark mode.

Angular Material dark theme

Step 3: Confirm overlays just work

In the old Material theming approach, dialogs and menus lived in an overlay container outside your root component. That meant you had to manually add a dark-mode class to OverlayContainer.

With Material 3 and color-scheme, that extra step is gone.

Because theme colors are CSS variables driven by color-scheme on the document, overlays inherit the same light/dark values automatically. Open a Material dialog after switching themes; it should already match.

To prove it in the demo app, we open a simple dialog from a button:

showDialog(): void {
  this.dialog.open(SampleDialogComponent, {
    width: '500px',
  });
}

No OverlayContainer injection. No class toggling on the overlay. Just set color-scheme, and Material follows.

Angular Material dialog in dark mode

Conclusion

And that's it! We now have proper dark mode for an Angular Material 3 app, including light, dark, and system preferences, with much less code than the old custom-theme approach.

To recap:

  1. Use one mat.theme() and set color-scheme: light dark
  2. Toggle themes by updating color-scheme (light, dark, or light dark)
  3. Skip overlay hacks; dialogs and menus follow automatically

If you want to go further, you can customize brand colors, typography, and density, or generate a full theme visually with Material Theme Builder.

The complete code for the sample app is available on this GitHub repository.

Updated for Angular 22 and Angular Material 3. The old dual-theme / .darkMode / OverlayContainer approach has been replaced with mat.theme(), light-dark(), and color-scheme.

Thanks for reading!

Bye :)

You may also like...

Loading comments...