23

I been reading a few articles on this but they seem to be conflicting in several different ways. I am hoping to re-create the same theme switching as the angular material documentation site for the latest version of angular material [5.0.0-rc0]

I have two custom themes, this is custom-theme.scss and there is light-custom-theme.scss which is nearly identical, sans the mat-dark-theme

@import '~@angular/material/theming';
$custom-theme-primary: mat-palette($mat-blue);
$custom-theme-accent: mat-palette($mat-orange, A200, A100, A400);
$custom-theme-warn: mat-palette($mat-red);
$custom-theme: mat-dark-theme($custom-theme-primary, $custom-theme-accent, $custom-theme-warn);

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

My styles.scss looks like so

@import '~@angular/material/theming';
@include mat-core();
@import 'custom-theme.scss';
@import 'light-custom-theme.scss';
.custom-theme {
  @include angular-material-theme($custom-theme);
}

.light-custom-theme {
  @include angular-material-theme($light-custom-theme);
}

And then it's called in the index.html <body class="mat-app-background">

Everything works fine when I do one theme. But I am trying to switch between the two. Adding both themes into angular-cli.json, the light-custom-theme takes over

"styles": [
  "styles.scss",
  "custom-theme.scss",
  "light-custom-theme.scss"
],

I have the following code in place in one of my components to handle toggling themes

toggleTheme(): void {
  if (this.overlay.classList.contains("custom-theme")) {
    this.overlay.classList.remove("custom-theme");
    this.overlay.classList.add("light-custom-theme");
  } else if (this.overlay.classList.contains("light-custom-theme")) {
    this.overlay.classList.remove("light-custom-theme");
    this.overlay.classList.add("custom-theme");
  } else {
    this.overlay.classList.add("light-custom-theme");
  }
}

But whenever it runs the theme remains the same. For what it is worth, there is already a "cdk-overlay-container" object at position 0 in overlay.classList

0:"cdk-overlay-container"
1:"custom-theme"
length:2
value:"cdk-overlay-container custom-theme" 

I am unsure how to debug this as the angular material documentation doesn't give me too much to work with, any help would be appreciative!

Thanks!

3 Answers 3

38

Here's an alternative solution for Angular 5.1+/Angular Material 5.0+.

*Edit: As noted, this still works in Angular 7+.

Working editable example - https://stackblitz.com/edit/dynamic-material-theming

In theme.scss, include a default theme(notice it isn't kept under a class name - this is so Angular will use it as the default), and then a light and dark theme.

theme.scss

@import '~@angular/material/theming';
@include mat-core();

// Typography
$custom-typography: mat-typography-config(
  $font-family: Raleway,
  $headline: mat-typography-level(24px, 48px, 400),
  $body-1: mat-typography-level(16px, 24px, 400)
);
@include angular-material-typography($custom-typography);

// Default colors
$my-app-primary: mat-palette($mat-teal, 700, 100, 800);
$my-app-accent:  mat-palette($mat-teal, 700, 100, 800);

$my-app-theme: mat-light-theme($my-app-primary, $my-app-accent);
@include angular-material-theme($my-app-theme);

// Dark theme
$dark-primary: mat-palette($mat-blue-grey);
$dark-accent:  mat-palette($mat-amber, A200, A100, A400);
$dark-warn:    mat-palette($mat-deep-orange);

$dark-theme:   mat-dark-theme($dark-primary, $dark-accent, $dark-warn);

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

// Light theme
$light-primary: mat-palette($mat-grey, 200, 500, 300);
$light-accent: mat-palette($mat-brown, 200);
$light-warn: mat-palette($mat-deep-orange, 200);

$light-theme: mat-light-theme($light-primary, $light-accent, $light-warn);

.light-theme {
  @include angular-material-theme($light-theme)
}

In the app.component file, include OverlayContainer from @angular/cdk/overlay. You can find Angular's documentation for this here https://material.angular.io/guide/theming; though their implementation is a little different. Please note, I also had to include OverlayModule as an import in app.module as well.

In my app.component file, I also declared @HostBinding('class') componentCssClass; as a variable, which will be used to set the theme as a class.

app.component.ts

import {Component, HostBinding } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { OverlayContainer} from '@angular/cdk/overlay';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {

  constructor(public overlayContainer: OverlayContainer) {}

  @HostBinding('class') componentCssClass;

  onSetTheme(theme) {
    this.overlayContainer.getContainerElement().classList.add(theme);
    this.componentCssClass = theme;
  }

}

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { HttpClientModule } from '@angular/common/http';

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';

import { AppComponent } from './app.component';

import { OverlayModule} from '@angular/cdk/overlay';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    BrowserAnimationsModule,
    MatCardModule,
    MatButtonModule,
    OverlayModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

Finally, call the onSetTheme function from your view.

app.component.html

<button mat-raised-button color="primary" (click)="onSetTheme('default-theme')">Default</button>
<button mat-raised-button color="primary" (click)="onSetTheme('dark-theme')">Dark</button>
<button mat-raised-button color="primary" (click)="onSetTheme('light-theme')">Light</button>

You might consider using an observable so that the functionality would be more dynamic.

Sign up to request clarification or add additional context in comments.

5 Comments

Do you happen to have a suggestion about how to preserve the selected theme on a browser reload?
@AaronLavers For a simple implementation, I would probably use cookies. A good package I've used is "ngx-cookie-service". You could store the theme name and then search for it OnInit in your component controller.
@AaronLavers if you would like for the theme to be consistent for the user across all browsers in addition (or instead of) to storing user selected theme in cookies/local storage you can store it server side and when loading the site get the info about user preferences from server
This example worked for me except for 1 detail: to switch between a light or dark background, I also had to change the style on the body element, which can be accessed through document.getElementsByTagName('body')[0]
Thanks very much. A minor change (v11.0) - need to remove the original class before adding new as the original appears to hang around causing problems. this.overlayContainer.getContainerElement().classList.remove(this.componentCssClass);
16

You have to use the getContainerElement method of OverlayContainer. Here's some example usage:

this.overlay.getContainerElement().classList.add('my-theme');

As for your style files, I strongly suggest removing this line for both custom-theme.scss and light-custom-theme.scss (you only need it for your classes in this case):

@include angular-material-theme($custom-theme); // Remove this line from custom-theme.scss and light-custom-theme.scss

If you also want to toggle the theme for your app, you should probably use this in the same toggleTheme method:

toggleTheme(): void {
  if (this.overlay.classList.contains("custom-theme")) {
    this.overlay.classList.remove("custom-theme");
    this.overlay.classList.add("light-custom-theme");
  } else if (this.overlay.classList.contains("light-custom-theme")) {
    this.overlay.getContainerElement().classList.remove("light-custom-theme");
    this.overlay.classList.add("custom-theme");
  } else {
    this.overlay.classList.add("light-custom-theme");
  }
  if (document.body.classList.contains("custom-theme")) {
    document.body.classList.remove("custom-theme");
    document.body.classList.add("light-custom-theme");
  } else if (document.body.classList.contains("light-custom-theme")) {
    document.body.classList.remove("light-custom-theme");
    document.body.classList.add("custom-theme");
  } else {
    this.overlay.classList.add("light-custom-theme");
  }
}

4 Comments

Awesome! this is nearly working as intended, however my body tag isn't updating and is staying in the light theme. It is appearing as class="mat-app-background custom-theme" but is still the light theme
Eh, figured it out, but are we really required to use a side-nav?
@Surreal You don't really have to place it in a sidenav, just make sure that the body has a class of mat-app-background in order for the theme to work correctly.
If you want to use the correct mat-app-background you can assign the custom-them-css-class selector to the document's root element within the function toggleTheme(): document.documentElement.classList.add("custom-theme"); With adding class="mat-app-background"to the <body> tag, the correct background color will take effect. As you call @include angular-material-theme($custom-theme); within your custom custom-them-css-class mixin, the mat-app-background css-selector will become a nested selector.
5

You cam always check how the theme picker is being implemented in https://material.angular.io/ and just do the same https://github.com/angular/material.angular.io/tree/master/src/app/shared/theme-picker by doing so you will have the everlasting solution as in case any breaking changes you always can lookup material docs source how to fix.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.