Angular Material Components Theming System: Complete Guide

Angular Material Components Theming System: Complete Guide

# Create a dark theme Now we will add a dark theme in the application. You can do this either in the same theme file or in separate theme files. ## Multiple themes in one file Defining multiple themes in a single file allows you to support multiple themes without having to manage loading of multiple CSS assets. The downside, however, is that your CSS will include more styles than necessary. To control which theme applies when, `@include` the mixins only within a context specified via CSS rule declaration. See the [documentation for Sass mixins](https://sass-lang.com/documentation/at-rules/mixin) for further background. Let's modify `src/styles/themes/_all.scss` to add a dark theme: ```scss // src/styles/themes/_all.scss // rest remains same // Define a dark theme $my-app-dark-theme: mat.define-dark-theme( ( color: ( primary: mat.define-palette(mat.$pink-palette), accent: mat.define-palette(mat.$blue-grey-palette), ), ) ); ``` Finally, make changes in `src/styles.scss` to apply dark theme when user prefers dark theme: ```scss // src/styles.scss // rest remains same @media (prefers-color-scheme: dark) { @include mat.core-color(themes.$my-app-dark-theme); @include mat.button-color(themes.$my-app-dark-theme); } ``` ## Multiple themes across separate files Now, instead of having all themes in one file, we can also have separate files for each theme. Let's see how we can achieve it. First, create a `dark.scss` file under `styles/themes` folder with below content: ```scss @use "@angular/material" as mat; @use "./all" as themes; .dark-theme { @include mat.core-color(themes.$my-app-dark-theme); @include mat.button-color(themes.$my-app-dark-theme); } ``` Notice that we are using a class selector `.dark-theme` to render a dark theme. ### Avoiding duplicated theming styles While creating `dark-theme`, instead of `core-theme` and `button-theme`, which we used in the original theme, we are using `core-color` and `button-color`. The reason behind that is we only want to change colors in `dark-theme` and every other style should remain the same. If we use theme mixins, it would generate all the styles again, which are not required. ### Changes for background and font color To complete the theme setup for background and font color, we will need to add class `mat-app-background` to the `` tag in `index.html`: ```html ``` ### Lazy load dark theme We don't want `dark-theme`` to be loaded with our application, instead we want it based on user preferences. Let's first exclude it from our bundle. To exclude `dark-theme` from bundle, we will set `inject` as `false` in `angular.json`'s `styles` array: ```json "styles": [ "src/styles.scss", { "bundleName": "dark-theme", "inject": false, "input": "src/styles/themes/dark.scss" }, ] ``` Next, to load the `dark-theme` based on user's selection, we will simply implement a service called `theme-manager` and whenever we want to change theme, we will simply call `changeTheme` from this service: ```typescript import { DOCUMENT } from '@angular/common'; import { Injectable, inject } from '@angular/core'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { take } from 'rxjs/operators'; import { BrowserStorageService } from './browser-storage.service'; const LOCAL_STORAGE_KEY = 'angular-material.dev'; @Injectable({ providedIn: 'root' }) export class ThemeManager { private document = inject(DOCUMENT); private browserStorage = inject(BrowserStorageService); private _isDarkSub = new BehaviorSubject(false); isDark$ = this._isDarkSub.asObservable(); private _window = this.document.defaultView; constructor() { this.setTheme(this.getPreferredTheme()); if (this._window !== null && this._window.matchMedia) { this._window .matchMedia('(prefers-color-scheme: dark)') .addEventListener('change', () => { const storedTheme = this.getStoredTheme(); if (storedTheme !== 'light' && storedTheme !== 'dark') { this.setTheme(this.getPreferredTheme()); } }); } } getStoredTheme = () => JSON.parse(this.browserStorage.get(LOCAL_STORAGE_KEY) ?? '{}').theme; setStoredTheme = (theme: string) => { const meta = JSON.parse(this.browserStorage.get(LOCAL_STORAGE_KEY) ?? '{}'); meta.theme = theme; this.browserStorage.set(LOCAL_STORAGE_KEY, JSON.stringify(meta)); }; getPreferredTheme = (): 'dark' | 'light' => { const storedTheme = this.getStoredTheme(); if (storedTheme) { return storedTheme; } if (this._window !== null && this._window.matchMedia) { return this._window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; } return 'light'; }; setTheme = (theme: string) => { if (this._window !== null && this._window.matchMedia) { if ( theme === 'auto' && this._window.matchMedia('(prefers-color-scheme: dark)').matches ) { this.document.documentElement.setAttribute('data-bs-theme', 'dark'); this._isDarkSub.next(true); } else { this.document.documentElement.setAttribute('data-bs-theme', theme); this._isDarkSub.next(theme === 'dark'); } this.setMaterialTheme(); } }; setMaterialTheme() { this.isDark$.pipe(take(1)).subscribe((isDark) => { if (isDark) { const href = 'dark-theme.css'; getLinkElementForKey('dark-theme').setAttribute('href', href); this.document.documentElement.classList.add('dark-theme'); } else { this.removeStyle('dark-theme'); this.document.documentElement.classList.remove('dark-theme'); } }); } removeStyle(key: string) { const existingLinkElement = getExistingLinkElementByKey(key); if (existingLinkElement) { this.document.head.removeChild(existingLinkElement); } } changeTheme(theme: string) { this.setStoredTheme(theme); this.setTheme(theme); } } function getLinkElementForKey(key: string) { return getExistingLinkElementByKey(key) || createLinkElementWithKey(key); } function getExistingLinkElementByKey(key: string) { return document.head.querySelector( `link[rel="stylesheet"].${getClassNameForKey(key)}` ); } function createLinkElementWithKey(key: string) { const linkEl = document.createElement('link'); linkEl.setAttribute('rel', 'stylesheet'); linkEl.classList.add(getClassNameForKey(key)); document.head.appendChild(linkEl); return linkEl; } function getClassNameForKey(key: string) { return `style-manager-${key}`; } ``` With above, we will also need a `browser-storage` service, it's code is given below: ```typescript import { Inject, Injectable, InjectionToken } from '@angular/core'; export const BROWSER_STORAGE = new InjectionToken('Browser Storage', { providedIn: 'root', factory: () => localStorage, }); @Injectable({ providedIn: 'root' }) export class BrowserStorageService { constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {} get(key: string) { return this.storage.getItem(key); } set(key: string, value: string) { this.storage.setItem(key, value); } remove(key: string) { this.storage.removeItem(key); } clear() { this.storage.clear(); } } ``` Codes for both, `theme-manager` and `browser-storage` are self-explanatory. Please modify it according to need of your application. ### Output after creating a dark-theme Now, let’s utilize the above service in `app.component.ts`: ```typescript import { Component, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; import { ThemeManager } from './theme-manager.service'; @Component({ selector: 'app-root', standalone: true, imports: [CommonModule, MatButtonModule, MatIconModule, MatMenuModule], templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], }) export class AppComponent { title = 'my-app'; themeManager = inject(ThemeManager); isDark$ = this.themeManager.isDark$; changeTheme(theme: string) { this.themeManager.changeTheme(theme); } } ``` As you have noticed, with `ThemeManager` service, we have also imported `MatIcon` and `MatMenu` modules. Let's make a few changes in template: ```html

Angular Material Theming System: Complete Guide

{{ (isDark$ | async) === true ? "dark_mode" : "light_mode" }} System Light Dark
Raised Accent Warn
``` Next, let's add `icon-theme` and `menu-theme` mixins in our styles: ```scss // src/styles.scss // rest remains same @include mat.icon-theme(themes.$my-app-light-theme); @include mat.menu-theme(themes.$my-app-light-theme); ``` ```scss // src/styles/themes/dark.scss // imports... .dark-theme { // rest remains same @include mat.icon-color(themes.$my-app-dark-theme); @include mat.menu-color(themes.$my-app-dark-theme); } ``` Let’s look at the output now: ![embedded video](https://res.cloudinary.com/dbgsyjnmu/video/upload/v1698428696/angular-material.dev/Untitled_video_-_Made_with_Clipchamp_7_bz0ftr.mp4 "output after adding dark theme") Notice that when we change theme, it changes colors and background colors of buttons and text. And also notice that `dark-theme.css`` is only included when the user switches to the dark theme.
Support Free Content Creation

Contributions & Support

Even though the courses and articles are available at no cost, your support in my endeavor to deliver top-notch educational content would be highly valued. Your decision to contribute aids me in persistently improving the course, creating additional resources, and maintaining the accessibility of these materials for all. I'm grateful for your consideration to contribute and make a meaningful difference!

Envelop
Don't miss any update

Stay up to date

Subscribe to the newsletter to stay up to date with articles, courses and much more!

Angular Material Dev

Angular Material Dev is one place stop for developers to learn about integrating Material Design in Angular applications like a pro.

Find us on X (Twitter)