DEV Community

Cover image for Angular Material Theming
Alex Patterson for CodingCatDev

Posted on • Originally published at ajonp.com on

Angular Material Theming

Original Post: https://ajonp.com/courses/angularmaterial/angular-material-theming/

I was a little rushed on this lesson/video so if you have any questions jump on the Slack Channel

Angular Material Theming

🌎 Demo: https://ajonp-lesson-10.firebaseapp.com/kitchensink

Angular Material is the officially supported components for implementing Google's Material Design System within Angular.
There are 4 pre-built themes that come with Angular Material

  • deeppurple-amber
  • indigo-pink
  • pink-bluegrey
  • purple-green We are going to take a look at creating our own theme and including this in with our default style, then using this to provide mixins for each of our components styles.

Probably the biggest complaint that I hear about Themes in Angular Material is the requirement for building an entire new set of css using the sass function, which bloats your style.css files. Please checkout Ionic CSS Variables to see a comparison of how this can be accomplished. As of writing this still remains an open issue on GitHub

Lesson Steps

  1. Kitchen Sink Component
  2. Angular Flex-Layout
  3. Angular Material Theme Creation
  4. Theme Selection Angular Service
  5. Sass Mixins
  6. Tools

Kitchen Sink Component

In English (maybe just USA) we say "Everythign but the kitchen sink", in programming we often will use the "Kitchensink" to refer to everything we can possible use on a site. For our example we will pull from the Angular Material Kitchensink so that we can highlight all of the possible components and what the theme does to them.

The components can be found on material.angular.io/components there are a few from the Component Dev Kit (CDK) as well.

Clone Repo

Usually I would offer up a guide to build this entire thing out (and I show this in the video if you want to follow along) but to simplify things I am recommend you just clone the Lesson 10 repo (or fork it and clone yours) and check it out for this walk through.

git clone https://github.com/AJONPLLC/lesson-10-angular-material-theming
Enter fullscreen mode Exit fullscreen mode

If you would like to follow along you can snage the Lesson 9 and work with me through the updates.

git clone https://github.com/AJONPLLC/lesson-9-angular-material-router-outlet
Enter fullscreen mode Exit fullscreen mode

Angular Flex-Layout

Angular FLex-Layout or fxLayout, offeres a great alternative to creating all the css for a flexlayout by hand. It is very similar to Bootstrap's Flex but I feel as though it is even more powerful.

This is a great example by tburleson:

Toolbar

For our example we used the nogrow option in the toolbar so that the material icon would stay shifted off to the right using fxFlex="nogrow".

sidenav.component.html

<mat-sidenav-container>
  <mat-sidenav opened="false" mode="over" #snav>
    <mat-nav-list>
      <mat-list-item>
        <h4
          matLine
          routerLink="/welcome"
          [routerLinkActiveOptions]="{ exact: true }"
          routerLinkActive="active-link"
        >
          Home
        </h4>
      </mat-list-item>
      <mat-list-item>
        <h4
          matLine
          routerLink="/kitchensink"
          [routerLinkActiveOptions]="{ exact: true }"
          routerLinkActive="active-link"
        >
          Kitchen Sink
        </h4>
      </mat-list-item>
      <mat-list-item>
        <h4 matLine routerLink="/books" routerLinkActive="active-link">
          Books
        </h4>
      </mat-list-item>
    </mat-nav-list>
  </mat-sidenav>
  <mat-sidenav-content>
    <mat-toolbar color="primary">
      <button
        type="button"
        aria-label="Toggle sidenav"
        mat-icon-button
        (click)="snavToggle(snav)"
      >
        <mat-icon>menu</mat-icon>
      </button>
      <span fxFlex>
        <a class="home-link" routerLink=".">{{ title }}</a>
      </span>
      <span fxFlex="nogrow">
        <button mat-icon-button [matMenuTriggerFor]="menu">
          <mat-icon>format_color_fill</mat-icon>
        </button>
        <mat-menu #menu="matMenu">
          <button mat-menu-item (click)="pickColor('')">
            <div class="color-swatch color-swatch-primary" mat-menu-item></div>
          </button>
          <button mat-menu-item (click)="pickColor('green')">
            <div class="color-swatch color-swatch-green" mat-menu-item></div>
          </button>
          <button mat-menu-item (click)="pickColor('pink')">
            <div class="color-swatch color-swatch-pink" mat-menu-item></div>
          </button>
          <button mat-menu-item (click)="pickColor('purple')">
            <div class="color-swatch color-swatch-purple" mat-menu-item></div>
          </button>
          <button mat-menu-item (click)="pickColor('yellow')">
            <div class="color-swatch color-swatch-yellow" mat-menu-item></div>
          </button>
          <button mat-menu-item (click)="pickColor('warn')">
            <div class="color-swatch color-swatch-warn" mat-menu-item></div>
          </button>
        </mat-menu>
      </span>
    </mat-toolbar>
    <router-outlet></router-outlet>
  </mat-sidenav-content>
</mat-sidenav-container>
Enter fullscreen mode Exit fullscreen mode

Kitchensink

In our kitchensink example we wrapped every component with a <span fxFlex>, then wrapped the entire component in <div style="max-width: 80%" fxLayout="column" fxLayoutAlign="center center"> which will allow everything to stay in the center and each of the fxFlex items to be within a flexbox. You can see that each span will have the style flex: 1 1 1e-09px;box-sizing: border-box; when rendered in the html.

Angular Material Theme Creation

You could use the 4 prebuilt themes that come with the @angular/material package, by just importing them into your style.css

@import "~@angular/material/prebuilt-themes/indigo-pink.css";
@import "~@angular/material/prebuilt-themes/deeppurple-amber.css";
@import "~@angular/material/prebuilt-themes/pink-bluegrey.css";
@import "~@angular/material/prebuilt-themes/purple-green.css";

Custom Theme

I prefer to add them directly so that you can create your own branding colors. You can't make a new unique site if you don't allow for your Brand's identity to show through (you know what I am talking about Marketing!).

The best site I have found for this is Material Design Palette Generator unlike Google's Material Color Tool, the DPG allows you to export direct to sass variables which is exactly what we need when creating an Angular Material Theme.

Make sure when you choose download (the down arrow), you then choose ANGULAR JS 2 (MATERIAL 2).
Download Design Palette Generator

Update theme file

You can then use all of these custom variables to create new themes. The sass variable should define the full spectrum along with contrast of each color in the palette.

Please make sure you include all of the material sass theming functions by adding this to the top of any theme file. You should only have this once in any project, so if this makes more sense in your style file using various mixins and include features please do this in a way that makes sense for your project.

@import '~@angular/material/theming';
@include mat-core();
Enter fullscreen mode Exit fullscreen mode

SASS Variables

angular-material-router-theme.scss

$md-purple: (
  50: #ffffff,
  100: #d3cbe7,
  200: #b2a5d5,
  300: #8974be,
  400: #775fb4,
  500: #674ea7,
  600: #5a4492,
  700: #4d3b7d,
  800: #403168,
  900: #342754,
  A100: #f9f7fd,
  A200: #b7a2ec,
  A400: #7c5dcb,
  A700: #7256b9,
  contrast: (
    50: #000000,
    100: #000000,
    200: #000000,
    300: #000000,
    400: #ffffff,
    500: #ffffff,
    600: #ffffff,
    700: #ffffff,
    800: #ffffff,
    900: #ffffff,
    A100: #000000,
    A200: #000000,
    A400: #ffffff,
    A700: #ffffff
  )
);
Enter fullscreen mode Exit fullscreen mode

Creation of the Sass variable for our new palette, here the mat-palette function is returning the color information and mappings and assigning it to $angular-material-router-app-purple.

$angular-material-router-app-purple: mat-palette($md-purple);
Enter fullscreen mode Exit fullscreen mode

New Material Theme

Once you have a new palette it is finally time to create a new Sass variable $angular-material-router-app-theme-purple that hosts your full theme information.

$angular-material-router-app-theme-purple: mat-dark-theme(
  $angular-material-router-app-purple,
  $angular-material-router-app-accent,
  $angular-material-router-app-warn
);
Enter fullscreen mode Exit fullscreen mode

Please note I have used the default accent and warn, but you can make these any color that you would like as well. Then whenever you use color="warn" it could be purple or gray instead of red, please aroudnd with switching out the variables in any theme.

Style Class Update

At this point we have only created a bunch of Sass variables that are not included in our app anywhere. In order to do this we will include them in a surrounding class, this can be done in the style.scss file, this should have been generated by the Angular CLI, or it is already in the styles folder if you cloned the repo.

Import SCSS Theme File

First make sure you are including the new theme file that holds all of our variables, by importing the scss @import 'angular-material-router-theme.scss';.

You can do this in any component to use the Sass variables.

Include Theme into class variable

Now we will include inside this class the entire theme (css) for our app.

...
.angular-material-router-app-theme-purple {
  $theme: $angular-material-router-app-theme-purple;
  @include angular-material-theme($theme);
}
...
Enter fullscreen mode Exit fullscreen mode

Sidenav Color Selector

Now that we have all of our theme colors defined we can update the sidenav button clicks styles to include the correct background color, this assigns each menu item the color and with a border-radius of 50% makes it a circle.

sidenav.component.scss


.color-swatch {
  position: relative;
  width: 36px;
  height: 36px;
  margin: 6px;
  border-radius: 50%;
  overflow: hidden;
}

.color-swatch-primary {
  background-color: mat-color($angular-material-router-app-primary, 500);
}
.color-swatch-primary:hover {
  background-color: mat-color(
    $angular-material-router-app-primary,
    500
  ) !important;
}
.color-swatch-green {
  background-color: mat-color($angular-material-router-app-green, 500);
}
.color-swatch-green:hover {
  background-color: mat-color(
    $angular-material-router-app-green,
    500
  ) !important;
}
.color-swatch-pink {
  background-color: mat-color($angular-material-router-app-pink, 500);
}
.color-swatch-pink:hover {
  background-color: mat-color(
    $angular-material-router-app-pink,
    500
  ) !important;
}
.color-swatch-purple {
  background-color: mat-color($angular-material-router-app-purple, 500);
}
.color-swatch-purple:hover {
  background-color: mat-color(
    $angular-material-router-app-purple,
    500
  ) !important;
}
.color-swatch-yellow {
  background-color: mat-color($angular-material-router-app-yellow, 500);
}
.color-swatch-yellow:hover {
  background-color: mat-color(
    $angular-material-router-app-yellow,
    500
  ) !important;
}
.color-swatch-warn {
  background-color: mat-color($angular-material-router-app-warn, 500);
}
.color-swatch-warn:hover {
  background-color: mat-color(
    $angular-material-router-app-warn,
    500
  ) !important;
}
Enter fullscreen mode Exit fullscreen mode

Theme Selection Angular Service

You can read up on Angular Services and why you should use them.

Create color-picker Service

I like to create all of my services in a folder called core, I will often put other things like models in this folder as well.

ng g s core/services/color-picker.service
Enter fullscreen mode Exit fullscreen mode

In this service we need to have an initial class set for the app in variable initialClass, if you follow the lesson video it shows how this works with local storage in the browser. This allows a person to set the theme and not need to select on refresh. So we look there first and if we cannot fild it we just add the initial class, which is what is also set to our BehaviorSubject. You will also notice we are updating something called OverlayContainer, this comes from the Material CDK and allows many of the floating panels to have style.

color-picker.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { OverlayContainer } from '@angular/cdk/overlay';

@Injectable({
  providedIn: 'root'
})
export class ColorPickerService {
  initialClass = 'angular-material-router-app-theme';
  colorClass$: BehaviorSubject<string> = new BehaviorSubject(this.initialClass);
  constructor(private overlayContainer: OverlayContainer) {
    const storageClass = localStorage.getItem('color-picker');
    console.log(storageClass);
    if (storageClass) {
      overlayContainer.getContainerElement().classList.add(storageClass);
      this.colorClass$.next(storageClass);
    } else {
      overlayContainer.getContainerElement().classList.add(this.initialClass);
    }
  }
  getColorClass() {
    return this.colorClass$;
  }
  setColorClass(className: string) {
    this.overlayContainer.getContainerElement().classList.forEach(css => {
      this.overlayContainer.getContainerElement().classList.remove(css);
    });
    this.overlayContainer.getContainerElement().classList.add(className);
    this.colorClass$.next(className);
    localStorage.setItem('color-picker', className);
  }
}
Enter fullscreen mode Exit fullscreen mode

Update App for Theme Class

Now that we have all the underling items taken care of we need to actually update the app's class on the fly. This is again why I like to create everything in Modules away from the app component, because it makes this part look very clean.

You will add the behavior subject to the entire app just by adding [ngClass]="themeClass | async". This will take the last Observable string for the class name and assign in when the service broadcasts.

app.component.html

<div [ngClass]="themeClass | async"><app-sidenav></app-sidenav></div>
Enter fullscreen mode Exit fullscreen mode

We of course need to tell our html where this came from by updating our app.component.ts file by using dependency injection to add our new color-service.

app.component.ts

import { ColorPickerService } from './core/services/color-picker.service';
import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  title = 'angular-material-router-outlet';
  themeClass;
  constructor(private colorPicker: ColorPickerService) {
    this.themeClass = this.colorPicker.getColorClass();
  }
  ngOnInit(): void {}
}
Enter fullscreen mode Exit fullscreen mode

Sass Mixins

The easiest way to create color on those items that dont allow the color directive, is to create a mixin based on our current theme's color. For instance in our kitchensink mat-grid-list does not offer color, but we can add it to the background by providing a mixin.

Add Mixin to Theme

angular-material-router-theme.scss

@mixin mat-grid-mixin($theme) {
  $primary: map-get(
    $map: $theme,
    $key: primary
  );

  mat-grid-tile {
    background-color: mat-color($primary, 500);
    color: mat-contrast($primary, 500);
  }
}
Enter fullscreen mode Exit fullscreen mode

Include the mixin @include mat-grid-mixin($theme); update when each theme class is selected. Now anything with mat-grid-tile will receive the background color of the current primary theme color. You can get any key that you want from the sass function map-get and use it in the mat-color and mat-contrast functions.

.angular-material-router-app-theme-purple {
  $theme: $angular-material-router-app-theme-purple;
  @include angular-material-theme($theme);
  @include mat-grid-mixin($theme);
}
Enter fullscreen mode Exit fullscreen mode

Create Mixin outside of Theme

Lets say that just in our kitchensink example we want to make all the buttons switch from a standard radius

normal radius button

to a more rounded radius

45px radius

We can add a new mixin file

angular-button-large-radius.scss

@mixin angular-button-large-radius {
  .mat-raised-button,
  .mat-stroked-button,
  .mat-flat-button {
    border-radius: 45px;
  }
}
Enter fullscreen mode Exit fullscreen mode

Then just include this into our kitchensink sass.

@import '../../../styles/angular-button-large-radius.scss';

@include angular-button-large-radius;
Enter fullscreen mode Exit fullscreen mode

Tools

Tools Used In Lesson

Additional Tools

Top comments (2)

Collapse
 
rsantoyodev profile image
Ricardo Santoyo

Hi, invite you testing Super javascript Stylesheets sjss, it will save you tons of time using fx-flex, or handling a lot of theming! check this demo in stackblitz

Collapse
 
jayeshvora profile image
jayesh-vora

I want to implement theme switcher in my MEAN stack application, with little caveat is that I don't want to include material theming and also without using CSS Variables.