Custom Theme for Angular Material Components Series: Part 3 — Apply Theme

Custom Theme for Angular Material Components Series: Part 3 — Apply Theme

This is the third article of Custom Theme for Angular Material Components Series. In the first and second, we understood how Angular Material’s and its components' themes work.

Apply Theme to Angular Material and its different Components.

Summary

This is the third article of Custom Theme for Angular Material Components Series. In the first and second, we understood how Angular Material’s and its components' themes work.

In this article we will proceed to modify default theme of components. Below is what we will be doing:

  1. Understand theme of MatToolbar
  2. Apply MatToolbar's theme to MatSidenav and MatDialog
  3. Create a different theme for MatSnackbar and nice styling for different kind of notifications (default, info, success, warning, error)

1. Understand theme of MatToolbar

Now as you know, how each components theme is included in our application, let’s move ahead and see MatToolbar's theme: _src/material/toolbar/toolbar-theme.scss:

...

@mixin mat-toolbar-color($config-or-theme) {
  $config: mat-get-color-config($config-or-theme);
  $primary: map-get($config, primary);
  $accent: map-get($config, accent);
  $warn: map-get($config, warn);
  $background: map-get($config, background);
  $foreground: map-get($config, foreground);

  .mat-toolbar {
    background: mat-color($background, app-bar);
    color: mat-color($foreground, text);

    &.mat-primary {
      @include _mat-toolbar-color($primary);
    }

    &.mat-accent {
      @include _mat-toolbar-color($accent);
    }

    &.mat-warn {
      @include _mat-toolbar-color($warn);
    }

    @include _mat-toolbar-form-field-overrides;
  }
}

@mixin mat-toolbar-typography($config-or-theme) {
  $config: mat-get-typography-config($config-or-theme);
  .mat-toolbar,
  .mat-toolbar h1,
  .mat-toolbar h2,
  .mat-toolbar h3,
  .mat-toolbar h4,
  .mat-toolbar h5,
  .mat-toolbar h6 {
    @include mat-typography-level-to-styles($config, title);
    margin: 0;
  }
}

...

@mixin mat-toolbar-theme($theme-or-color-config) {
  ...
      @include mat-toolbar-color($color);
      ...
      @include _mat-toolbar-density($density);
      ...
      @include mat-toolbar-typography($typography);
  ...
}
  1. mat-toolbar-theme: This the main mixin which is responsible to apply theme to MatToolbar. In this, 3 other mixins are included:
  2. mat-toolbar-color: In this mixin first they are fetching all theme colors using palette functions. Then, default background and font-colors are assigned. For each themed toolbar, they are creating classes .mat-primary , .mat-accent and .mat-warn.
  3. _mat-toolbar-density: This mixin is responsible to assign responsive scale and height.
  4. mat-toolbar-typography: As the name says, typography of MatToolbar is coming from this.

Let's apply theme of MatToolbar to MatSidenav and MatDialog.

2. Apply MatToolbar's theme to MatSidenav and MatDialog

MatSidenav

MatToolbar has an attribute/property called color that's the reason, when we directly call color="primary" (or [color]="themeColor" in our application) it applies the respective theme. But, color attribute is not there for MatSidenav. So, we would need to handle that.

To really check if MatSidenav supports color attribute or not, you can try by adding color="primary" in mat-sidenav tag. You will see nothing changed in the output.

To actually see the error, change it to [color]="themeColor". You will see the error in browser console: Error: Template parse errors: Can't bind to 'color' since it isn't a known property of 'mat-sidenav'.

So, to assign themeColor to color attribute of MatSidenav, we can do the Attribute Binding. Add [attr.color]="themeColor" to <mat-sidenav> and keep the rest of the content same:

<mat-sidenav #sidenav mode="side" opened [attr.color]="themeColor">
    ...
</mat-sidnav>

👉 You can set the value of an attribute directly with an attribute binding. You must use attribute binding when there is no element property to bind. You can read more about the same at Attribute Binding Guide.

As you would see, attribute biding won’t cause any errors, but as we haven’t done anything related to styling, it won’t make any difference to the output. Let’s do that.

For custom component theme, we will follow this naming convention: <component-name>.scss-theme.scss.

Let's create a stylesheet at: src/app/shared/components/sidenav/sidenav.component.scss-theme.scss:

// src\app\shared\components\sidenav\sidenav.component.scss-theme.scss

@import "~@angular/material/theming";
@mixin sidenav-component-theme($config-or-theme) {
  // retrieve variables from theme
  // (all possible variables, use only what you really need)
  $config: mat-get-color-config($config-or-theme);
  $primary: map-get($config, primary);
  $accent: map-get($config, accent);
  $warn: map-get($config, warn);
  $foreground: map-get($config, foreground);
  $background: map-get($config, background);

  .mat-drawer {
    // let's take mat-toolbar's default theme
    background-color: mat-color($background, app-bar);
    color: mat-color($foreground, text);

    $color-list: (
      "primary": $primary,
      "accent": $accent,
      "warn": $warn,
    );

    // now, mat-toolbar's colored themes
    @each $key, $val in $color-list {
      &[color="#{$key}"] {
        @include _mat-toolbar-color($val);
        .mat-list-base {
          .mat-list-item {
            color: mat-color($val, default-contrast);
          }
        }
      }
    }
  }
}

As you can see, we are importing ~@angular/material/theming, so that we can use some basic Material mixins, like mat-color and MatToolbar's colored mixin _mat-toolbar-color. We are creating a mixin called sidenav-component-theme to handle our SidenavComponent's theme. In our mixin, we are first fetching all theme colors using map-get. Then, in class .mat-sidenav, which is applied to <mat-sidenav> tag, we are giving MatToolbar default theme and then colored themes to color attributes.

Now, we need to include sidenav-component-theme mixin src/custom-component-themes.scss:

// custom-component-themes.scss

// import custom components themes

// you only have to add additional components here (instead of in every theme class)

@import "./app/shared/components/sidenav/sidenav.component.scss-theme.scss";

@mixin custom-components-theme($theme) {
  @include sidenav-component-theme($theme);
}

To allow dark theme in our custom-components-theme, we also need to change it in src/styles.scss:

...
.dark-theme {
  @include angular-material-color($dark-theme);
  @include custom-components-theme($dark-theme); // 👈 added
}
...

Let's run the project: ng serve -o and you should see the output like below:

Output after applying `MatToolbar`'s theme to `MatSidenav`

MatDialog

First, let's import MatDialogModule in MaterialModule:

// src/app/material/material.module.ts

...
import { MatDialogModule } from '@angular/material/dialog';

@NgModule({
  exports: [
    ...,
    MatDialogModule
  ]
})

Then, create a component using terminal command:

ng g c shared/components/dialog
<!-- src\app\shared\components\dialog\dialog.component.html -->

<h2 mat-dialog-title [attr.color]="data.themeColor">Dialog Header</h2>
<mat-dialog-content class="mat-typography">
  <div class="mat-dialog-content-body">
    <h3>Sub Header</h3>
    <p>
      Lorem ipsum dolor sit amet consectetur adipisicing elit. Eveniet sunt ratione eligendi ab esse dolorum delectus
      aliquid, doloribus perferendis dolores impedit laudantium voluptatum cum ad odit eius harum saepe commodi.
    </p>
  </div>
</mat-dialog-content>
<mat-dialog-actions align="end">
  <button mat-button mat-dialog-close>Close</button>
</mat-dialog-actions>

Note that we have added [attr.color]="data.themeColor" in h2[mat-dialog-title]. That will help us to apply dynamic theme to header.

// src\app\shared\components\dialog\dialog.component.ts

import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';

@Component({
  selector: 'app-dialog',
  templateUrl: './dialog.component.html',
  styleUrls: ['./dialog.component.scss'],
})
export class DialogComponent implements OnInit {
  constructor(@Inject(MAT_DIALOG_DATA) public data: DialogData) {}

  ngOnInit(): void {}
}
export interface DialogData {
  themeColor: string;
}

We are injecting data using MAT_DIALOG_DATA, this will be coming from where we make a function to open the dialog.

We will also create a theme file for dialog at : src/app/shared/components/dialog/dialog.component.scss-theme.scss:

// src\app\shared\components\dialog\dialog.component.scss-theme.scss

@import "~@angular/material/theming";
@mixin dialog-component-theme($config-or-theme) {
  // retrieve variables from theme
  // (all possible variables, use only what you really need)
  $config: mat-get-color-config($config-or-theme);
  $primary: map-get($config, primary);
  $accent: map-get($config, accent);
  $warn: map-get($config, warn);
  $foreground: map-get($config, foreground);
  $background: map-get($config, background);
  .custom-dialog {
    .mat-dialog-container {
      padding-top: 0px;
      .mat-dialog-title {

        // below is to make header take some space
        padding: 12px 24px;
        margin-bottom: 0;
        margin-left: -24px;
        margin-right: -24px;

        // let's take mat-toolbar's default theme
        background-color: mat-color($background, app-bar);
        color: mat-color($foreground, text);

        // now, mat-toolbar's colored themes
        $color-list: (
          "primary": $primary,
          "accent": $accent,
          "warn": $warn,
        );
        @each $key, $val in $color-list {
          &[color="#{$key}"] {
            @include _mat-toolbar-color($val);
          }
        }
      }
    }
  }
}

We have to make some adjustments to make mat-dialog-title take some spacing, so that background color is nicely visible. Also note that we have created a class .custom-dialog, we will use this when creating dialog.

Next, include this theme in our src/custom-component-themes.scss:

// custom-component-themes.scss

// import custom components themes

// you only have to add additional components here (instead of in every theme class)

@import "./app/shared/components/sidenav/sidenav.component.scss-theme.scss";
@import "./app/shared/components/dialog/dialog.component.scss-theme.scss"; // 👈 Added

@mixin custom-components-theme($theme) {
  @include sidenav-component-theme($theme);
  @include dialog-component-theme($theme); // 👈 Added
}

AppComponent

Let's open MatDialog from app.component.html. Just change the content to:

<!-- src\app\app.component.html -->

<app-sidenav #sidenav>
  <div class="container">
    <h1 class="mat-display-3">Main Content</h1>
    <p>
      Lorem ipsum dolor sit amet consectetur adipisicing elit. Reiciendis nobis illum animi temporibus omnis.
      Consequatur qui fugit facilis reiciendis deserunt, debitis beatae! Illo dolorem asperiores nisi excepturi eum
      veritatis cupiditate.
    </p>
    <button mat-raised-button [color]="sidenav.themeColor" (click)="openDialog(sidenav.themeColor)">Open Dialog</button>
  </div>
</app-sidenav>

Apart from creating a button to open dialog, we have also added a TemplateRef to <app-sidenav> so that we can access SidnavComponent's themeColor property and pass it on to MatDialog.

We also need to add a function openDialog in app.component.ts:

// src\app\app.component.ts

import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogComponent } from './shared/components/dialog/dialog.component';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  title = 'theming-material-components';

  constructor(private dialog: MatDialog) {}

  openDialog(themeColor: 'primary' | 'accent' | 'warn'): void {
    const dialogRef = this.dialog.open(DialogComponent, {
      panelClass: 'custom-dialog',
      data: {
        themeColor,
      },
    });
  }
}

Let's look at the output now:

Output after applying `MatToolbar`'s theme to `MatDialog`

3. Create a different theme for MatSnackbar and create nice styling for different kind of notifications (default, info, success, warning, error)

First, import MatSnackBarModule in MaterialModule:

// src/app/material/material.module.ts

...
import { MatSnackBarModule } from '@angular/material/snack-bar';

@NgModule({
  exports: [
    ..,
    MatSnackBarModule
  ],
})
export class MaterialModule {}

Then, let's create a notification service, which will help us to call notifications from anywhere in the application:

ng g s shared/services/notification

We will change the content of service to below:

// src\app\shared\services\notification.service.ts

import { Injectable } from '@angular/core';
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar';

@Injectable({
  providedIn: 'root',
})
export class NotificationService {
  constructor(private readonly snackBar: MatSnackBar) {}

  default(message: string, isHandset?: boolean): void {
    this.show(
      message,
      {
        duration: 2000,
        panelClass: 'default-notification-overlay',
      },
      isHandset
    );
  }

  info(message: string, isHandset?: boolean): void {
    this.show(
      message,
      {
        duration: 2000,
        panelClass: 'info-notification-overlay',
      },
      isHandset
    );
  }

  success(message: string, isHandset?: boolean): void {
    this.show(
      message,
      {
        duration: 2000,
        panelClass: 'success-notification-overlay',
      },
      isHandset
    );
  }

  warn(message: string, isHandset?: boolean): void {
    this.show(
      message,
      {
        duration: 2500,
        panelClass: 'warning-notification-overlay',
      },
      isHandset
    );
  }

  error(message: string, isHandset?: boolean): void {
    this.show(
      message,
      {
        duration: 3000,
        panelClass: 'error-notification-overlay',
      },
      isHandset
    );
  }

  private show(message: string, configuration: MatSnackBarConfig, isHandset?: boolean): void {
    // If desktop, move it to top-right
    if (!isHandset) {
      configuration.horizontalPosition = 'right';
      configuration.verticalPosition = 'top';
    }

    this.snackBar.open(message, null, configuration);
  }
}

Note the use of panelClass. For each type of notification, we are adding different panelClass, which will help us in styling.

Next, create a style theme file: src\app\shared\services\notification.scss-theme.scss:

// src\app\shared\services\notification.scss-theme.scss

@mixin notification-theme($notifications-theme) {
  $default-color: map-get($notifications-theme, default);
  $info-color: map-get($notifications-theme, info);
  $success-color: map-get($notifications-theme, success);
  $warn-color: map-get($notifications-theme, warning);
  $error-color: map-get($notifications-theme, error);
  .default-notification-overlay,
  .info-notification-overlay,
  .success-notification-overlay,
  .warning-notification-overlay,
  .error-notification-overlay {
    border-left: 4px solid;
    &::before {
      font-family: "Material Icons";
      float: left;
      font-size: 24px;
      // because we want spaces to be considered in content
      // https://stackoverflow.com/questions/5467605/add-a-space-after-an-element-using-after
      white-space: pre;
    }
  }
  .default-notification-overlay {
    border-left-color: $default-color;
    &::before {
      color: #fff;
    }
  }
  .info-notification-overlay {
    border-left-color: $info-color;
    &::before {
      content: "\e88e  ";
      color: $info-color;
    }
  }
  .success-notification-overlay {
    border-left-color: $success-color;
    &::before {
      content: "\e86c  ";
      color: $success-color;
    }
  }
  .warning-notification-overlay {
    border-left-color: $warn-color;
    &::before {
      content: "\e002  ";
      color: $warn-color;
    }
  }
  .error-notification-overlay {
    border-left-color: $error-color;
    &::before {
      content: "\e000  ";
      color: $error-color;
    }
  }
}

Notice the use of $notifications-theme. That would be a map of colors needed for notifications' left borders. As you can see in the theme file, I have added relevant icon to indicate it's importance.

👉 These icons are from Material Icons' Codepoints, you can change them if you wish.

Let's create that map of colors in src/theme.scss. Add below content:

// src/theme.scss

...
// Theme for notifications / snackbar
$notifications-theme: (
  default: #fff,
  info: mat-color(mat-palette($mat-blue), 400),
  success: mat-color(mat-palette($mat-green), 400),
  warning: mat-color(mat-palette($mat-yellow), 400),
  error: mat-color(mat-palette($mat-red), 400),
);

Of course, you can use colors of your choice. We will also need to export this theme to styles.scss and eventually to custom-component-theme.scss and notification.scss-theme.scss. Let’s do that.

First, let's add an argument for notifications theme in custom-components-theme and also import notification-theme from src/app/shared/services/notification.scss-theme.scss:

// custom-component-themes.scss

// import custom components themes

// you only have to add additional components here (instead of in every theme class)

@import "./app/shared/components/sidenav/sidenav.component.scss-theme.scss";
@import "./app/shared/components/dialog/dialog.component.scss-theme.scss";
@import "./app/shared/services/notification.scss-theme.scss"; // 👈 Added

@mixin custom-components-theme($theme, $notifications-theme) {
  @include sidenav-component-theme($theme);
  @include dialog-component-theme($theme);
  @include notification-theme($notifications-theme); // 👈 Added
}

Next, in styles.scss, add our new theme in custom-component-theme:

// src\styles.scss
...
@include custom-components-theme($theming-material-components-theme, $notifications-theme); // 👈 changed

...

.dark-theme {
  @include angular-material-color($dark-theme);
  @include custom-components-theme($dark-theme, $notifications-theme); // 👈 changed
}

...

Let’s open a notification on a button click from our app.component.html:

<!-- src\app\app.component.html -->

<app-sidenav #sidenav>
  <div class="container">
    <h1 class="mat-display-3">Main Content</h1>
    <p>
      Lorem ipsum dolor sit amet consectetur adipisicing elit. Reiciendis nobis illum animi temporibus omnis.
      Consequatur qui fugit facilis reiciendis deserunt, debitis beatae! Illo dolorem asperiores nisi excepturi eum
      veritatis cupiditate.
    </p>
    <button mat-raised-button [color]="sidenav.themeColor" (click)="openDialog(sidenav.themeColor)">Open Dialog</button>

    <!-- 🚨 A button to show notification -->  
    <button mat-raised-button [color]="sidenav.themeColor" (click)="openNotification()">
      Open Notification
    </button>
  </div>
</app-sidenav>

Modifications are required in app.component.ts file, too:

// src\app\app.component.ts

import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { DialogComponent } from './shared/components/dialog/dialog.component';
import { NotificationService } from './shared/services/notification.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
})
export class AppComponent {
  title = 'theming-material-components';

  // 🚨 - NotificationService is injected
  constructor(private dialog: MatDialog, private notification: NotificationService) {}

  openDialog(themeColor: 'primary' | 'accent' | 'warn'): void {
    const dialogRef = this.dialog.open(DialogComponent, {
      panelClass: 'custom-dialog',
      data: {
        themeColor,
      },
    });
  }

  // 🚨 - Simple function to show notifications
  openNotification(): void {
    this.notification.default('Default Notification');
  }
}

If you change this.notification.default(…) to info(…), success(…), warn(…) or error(…), you would see respective outputs:

Notifications output

Great!!! With this, we're done with what we planned.

Conclusion

So, let's quickly revise what we learned in this series:

Part 1 - Create Theme

✔️ Create an Angular Project using Angular CLI and add Angular Material

✔️ Understand Angular Material Custom Theme

✔️ Create Base Theme Files

✔️ Update Project Structure with few new modules

✔️ Create basic UI skeleton

Part 2 - Understand Theme

️️️️✔️ We saw how theme generation works by taking an in-depth look into Angular Material Component's Github repo

Part 3 - Apply Theme

️️️️✔️ Understand theme of MatToolbar

️️️️✔️ Apply MatToolbar's theme to MatSidenav and MatDialog

️️️️✔️ Create a different theme for MatSnackbar and nice styling for different kind of notifications (default, info, success, warning, error)

Thank You,

for reading this article. This was the last part of the series. I hope this series gave you nice idea about Angular Material Theming.

The code is available at Github: shhdharmen/indepth-theming-material-components. Please let me know your thoughts and feedbacks in the comments.

Did you find this article valuable?

Support Dharmen Shah by becoming a sponsor. Any amount is appreciated!