Dynamic themes in Angular Material

Erik Tallang
Compendium
Published in
9 min readJun 19, 2018

A working demo of the dynamic themes we create through this article can be found here, while the source code is located here.

Setting the scene

If you have used Angular Material, you may have configured your own theme at some point. You may also have observed that you can use either the mat-light-theme or the mat-dark-theme to define your theme, as such:

@import '~@angular/material/theming';

@include mat-core();
$primary: mat-palette($mat-indigo);
$accent: mat-palette($mat-pink, A200, A100, A400);
$warn: mat-palette($mat-red);
$theme: mat-light-theme($primary, $accent, $warn); // Or mat-dark-theme

@include angular-material-theme($theme);

This will give you either a light- or dark-theme on your Angular Material components, depending on your choice. Unfortunately, there exists little information on how to change the theme dynamically, without having to refresh the page. I did a little research and found a solution to creating dynamic themes, which to many may seem obvious, while others may find it useful. Through this article we will create a minimal Angular application step by step, using Angular CLI to create a web application with dynamic themes. I will not describe the basic setup of an Angular application with Angular Material, since this is described in detail in their respective docs.

NB: Make sure that you select SCSS as your default styling extension when creating your application. This can be done through an argument in the Angular CLI:

ng new theme-demo --style scss

The basic stuff

After initializing the Angular app using Angular CLI, and adding Angular Material and Angular CDK as a dependency, we’re going to make a SCSS-file for our material theme. We’re placing it in a styles-folder, just for the sake of structure since we’ll add more theme-style files later.

touch src/styles/material-theme.scss

Configure your theme in material-theme.scss:

@import '~@angular/material/theming';

@include mat-core();

$primary: mat-palette($mat-indigo);
$accent: mat-palette($mat-pink, A200, A100, A400);
$warn: mat-palette($mat-red);
$theme: mat-light-theme($primary, $accent, $warn);

@include angular-material-theme($theme);

Finish of the theme configuration by importing our material style in styles.scss:

@import './styles/material-theme';

We should now be done with the basic setup. We can test this by adding a toolbar to our page.

Creating our first component

Create a module with a corresponding component, using Angular CLI:

ng g module toolbar
ng g component toolbar

Import the MatToolbarModule in our new module, in order to use the toolbar provided by Angular Material:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatToolbarModule } from '@angular/material/toolbar';

import { ToolbarComponent } from './toolbar.component';

@NgModule({
imports: [
CommonModule,
MatToolbarModule
],
declarations: [ ToolbarComponent ],
exports: [ ToolbarComponent ]
})
export class ToolbarModule { }

Use the Angular Material toolbar in our toolbar component template:

<mat-toolbar color="primary">
<mat-toolbar-row>
<h1>Theme Demo</h1>
</mat-toolbar-row>
</mat-toolbar>

In order to use this toolbar, we need to import the toolbar module in our app module:

import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { CommonModule } from '@angular/common';

import { ToolbarModule } from './toolbar/toolbar.module';
import { AppComponent } from './app.component';

@NgModule({
declarations: [ AppComponent ],
imports: [
CommonModule,
BrowserAnimationsModule,
ToolbarModule
],
bootstrap: [ AppComponent ]
})
export class AppModule { }

And then render the toolbar component in app.component.html by referencing the selector for the toolbar component:

<app-toolbar></app-toolbar>

You should now have something similar to the image below. Nice!

Our toolbar is now visible on the top of our page

Almost there (regarding the setup)

As a final step in the setup of the basic application, we need to wrap our entire page with the CSS-class ‘mat-app-background’. According to the Angular Material documentation, this CSS class will provide a default background color for your application, and will also change according to your theme. We can incporporate this CSS-class in our application by adding a div-element around the content in our app.component.html:

<div class="mat-app-background">
<app-toolbar></app-toolbar>
</div>

The good stuff

At this point, we have a web page with a toolbar and a background color that is managed by Angular Material. Our next step is to add a second theme, the dark theme.

Adding our dark theme

A new theme can be configured within a CSS-class and used in your application by referencing the CSS-class in your HTML. Add a new theme in our material-theme.scss:

@import '~@angular/material/theming';

@include mat-core();

$primary: mat-palette($mat-indigo);
$accent: mat-palette($mat-pink, A200, A100, A400);
$warn: mat-palette($mat-red);
$theme: mat-light-theme($primary, $accent, $warn);

@include angular-material-theme($theme);

// Our dark theme
.dark-theme {
color: $light-primary-text;
$dark-primary: mat-palette($mat-yellow);
$dark-accent: mat-palette($mat-amber, A400, A100, A700);
$dark-warn: mat-palette($mat-red);
$dark-theme: mat-dark-theme($dark-primary, $dark-accent, $dark-warn);

@include angular-material-theme($dark-theme);
}

The reference to $light-primary-text is a SCSS-variable found in the Angular Material theming. You may of course choose font color for your dark theme as you prefer. We can now use your new theme in our app.component.html:

<div class="dark-theme">
<div class="mat-app-background">
<app-toolbar></app-toolbar>
</div>
</div>

If you check out your changes, you should now see your dark theme applied to your application. Making progress!

Making it toggle

Our dynamic theme is no good if there aren’t any controls to toggle it! So lets add a slide toggle to our toolbar.component.html:

<mat-toolbar color="primary">
<mat-toolbar-row>
<h1>Theme Demo</h1>
<mat-slide-toggle>Dark theme</mat-slide-toggle>
</mat-toolbar-row>
</mat-toolbar>

A slide toggle should now be visible in your toolbar. In the working demo found in the top of this article, I have taken the liberty of moving the slide-toggle to the right side of the toolbar. This is only a styling preference and can be accomplished by using flexbox on the mat-toolbar-row. See the source code for an example.

The Core piece

A common pattern in Angular for communication between distant components, is to use a shared service. This pattern (and other communication patterns) is described in detail in the Angular documentation. In regard of the Core module pattern, we can create a Core module in our application, and create a service to administer the state of the dark-theme:

ng g module core
ng g service core/services/theme

To make our theme service publish changes to all components that listen, we use rxjs:

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class ThemeService {
private _darkTheme = new Subject<boolean>();
isDarkTheme = this._darkTheme.asObservable();

setDarkTheme(isDarkTheme: boolean): void {
this._darkTheme.next(isDarkTheme);
}
}

Thats really all there is to it. This service will provide an observable isDarkTheme that components can subscribe to in order to know if the dark theme is enabled or not. Controllers that wish to change the state of the dark theme, can change it by calling the setDarkTheme-function and provide a new state. Remeber to add our theme service to the providers-array in the Core module, and finally import the Core module in the App module.

Use our theme service

Our toggle slider to enable dark theme doesn’t serve much of a purpose yet. That’s about to change. Inject the theme service in the toolbar component, and call the setDarkTheme-function each time the slide toggle changes:

import { Component, OnInit } from '@angular/core';
import { ThemeService } from '../core/services/theme.service';
import { Observable } from 'rxjs/Observable';

@Component({
selector:'app-toolbar',
templateUrl:'./toolbar.component.html',
styleUrls: ['./toolbar.component.scss']
})
export class ToolbarComponent implements OnInit {
isDarkTheme: Observable<boolean>;

constructor(private themeService: ThemeService) { }

ngOnInit() {
this.isDarkTheme = this.themeService.isDarkTheme;
}

toggleDarkTheme(checked: boolean) {
this.themeService.setDarkTheme(checked);
}
}
<mat-toolbar color="primary">
<mat-toolbar-row>
<h1>Theme Demo</h1>
<mat-slide-toggle [checked]="isDarkTheme | async" (change)="toggleDarkTheme($event.checked)">Dark theme</mat-slide-toggle>
</mat-toolbar-row>
</mat-toolbar>

This ought to do it! Toggling the slide toggle will now call setDarkTheme in the ThemeService with the new state. We have also taken the liberty of subscribing to the isDarkTheme-observable, and setting the checked property on the slide toggle to the isDarkTheme value. Notice the use of the async pipe here as well, as it is super useful for unwrapping the boolean observable, and helps us manage potential memory leaks and unexpected behavior by unsubscribing when the component is destroyed.

The finishing touch

The only thing left is to add the pieces together. We already have the dark-theme CSS-class in our app.component.html, and we have the ThemeService that emits a new dark-theme state each time it changes. Let’s inject the ThemeService in our app.component.ts:

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';

import { ThemeService } from './core/services/theme.service';

@Component({
selector:'app-root',
templateUrl:'./app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
isDarkTheme: Observable<boolean>;

constructor(private themeService: ThemeService) { }

ngOnInit() {
this.isDarkTheme = this.themeService.isDarkTheme;
}
}

Finish it off by dynamically adding the .dark-theme CSS-class when the isDarkTheme property is true:

<div [ngClass]="{'dark-theme': isDarkTheme | async}">
<div class="mat-app-background">
<app-toolbar></app-toolbar>
<div class="content">
</div>
</div>
</div>

I have also taken the liberty of adding a div with the CSS-class .content. This is simply a wrapper for the page content, adding padding around it.

The next level

Adding Angular Material components within our application will now automatically adapt to the selected theme; it’s so easy it’s not fun. That’s why we’ll take this to the next level by creating a custom component that adapts to the theme.

Create the tile

Let’s create a simple tile component for our application. This is only for illustrating the concept, you may naturally create whatever kind of component you like.

ng g component src/app/tile

Change the tile template into the following:

<div class="tile">
<p><ng-content></ng-content></p>
</div>

The ng-content element allows us to insert custom content when using the tile component. Add a tile in the app.component.html:

<div [ngClass]="{'dark-theme': isDarkTheme | async}">
<div class="mat-app-background">
<app-toolbar></app-toolbar>
<div class="content">
<app-tile>Test</app-tile>
</div>
</div>
</div>

To make the tile adapt to the theme, it needs to have the theme provided each time it changes. We can accomplish this by creating a SCSS-mixin in our tile.component.scss:

@import '~@angular/material/theming';

@mixin tile-theme($theme) {
$accent: map-get($theme, accent);
$background: map-get($theme, background);
$background-color: mat-color($background, card);

.tile {
@includemat-elevation(2);
background-color: mat-color($accent);
border: 1rem solid $background-color;
padding: 2rem;
min-height: 200px;
min-width: 200px;

p {
color: $background-color;
line-height: 2.4rem;
font-size: 2rem;
}
}
}

This mixin needs to be called each time the theme changes. This is actually the same thing that happens when we call the angular-material-theme in our material-theme.scss. Each time the theme changes, the Angular Material components is provided the new theme through a mixin, which updates the colors on all the Angular Material components. We can adapt this pattern in our own application, by creating a new SCSS-file in our styles-folder:

touch src/styles/component-themes.scss

Add the following to our new SCSS-file:

@import '../app/tile/tile.component';

@mixin component-themes($theme) {
@include tile-theme($theme);
}

This is a bit overkill, since we only have a single component that adapts to the theme. This is however a good practice in larger applications, as the amount of components requiring the theme grows.

Back in our material-theme.scss we can now call our mixin and provide the theme:

@import '~@angular/material/theming';
@import './component-themes';

@include mat-core();

$primary: mat-palette($mat-indigo);
$accent: mat-palette($mat-pink, A200, A100, A400);
$warn: mat-palette($mat-red);
$theme: mat-light-theme($primary, $accent, $warn);

@include angular-material-theme($theme);
@include component-themes($theme);

// Our dark theme
.dark-theme {
color: $light-primary-text;
$dark-primary: mat-palette($mat-yellow);
$dark-accent: mat-palette($mat-amber, A400, A100, A700);
$dark-warn: mat-palette($mat-red);
$dark-theme: mat-dark-theme($dark-primary, $dark-accent, $dark-warn);

@include angular-material-theme($dark-theme);
@include component-themes($dark-theme);
}
Light theme
Dark theme

Final words

At the core of this article, the theme service is the secret ingredient that makes the dynamic theming possible. We have only scrathed the surface of the capabilities of this pattern, as one can imagine not only having a true/false state for the theme, but having an enum or other sorts for providing multiple themes. Or how about multiple observables that can control the theme of different components. I have also provided a second slide toggle in the working demo, to illustrate how multiple controls can change the theme, and still maintain the correct state by subscribing to the isDarkTheme observable. The possibilities here are endless, and are only limited by our imagination.

--

--