DEV Community

loading...
Cover image for Extending Angular Material Theme System

Extending Angular Material Theme System

martinmcwhorter profile image Martin McWhorter Updated on ・3 min read

The Angular Material project provides a powerful theming system. This theme system provides a few palettes: Primary, Accent and Warn. It may not be obvious how to extend the theme system to provide additional palettes -- or how to then use those extended palettes.

Here we will go through how to

  1. Add palettes to a theme
  2. Use these additional palettes

Theme System

First we have to familiarize ourselves with how the theme system works before we can extend it.

The theme is constructed as a set of SCSS maps. A map in SCSS is just an array of key-value pairs. Similar to a Map in Java, a Dictionary in C# or a Record in TypeScript.

If we take a look at the source we can see the functions for creating a light or dark theme just return a map containing the three material palates, a foreground and background maps, and an is-dark boolean.

@function mat-light-theme($primary, $accent, $warn: mat-palette($mat-red)) {
  @return (
    primary: $primary,
    accent: $accent,
    warn: $warn,
    is-dark: false,
    foreground: $mat-light-theme-foreground,
    background: $mat-light-theme-background,
  );
}

So we can add our successpalette simply by wrapping the function and adding our new bit in.

@function my-light-theme($primary, $accent, $warn: mat-palette($mat-red), $success: mat-palette($mat-green)) {
  @return map-merge(mat-light-theme($primary, $accent, $warn), (success: $success));
}

We can now repeat this for the dark theme.

@function my-dark-theme($primary, $accent, $warn: mat-palette($mat-red), $success: mat-palette($mat-green)) {
  @return map-merge(mat-dark-theme($primary, $accent, $warn), (success: $success));
}

In our style.scss where we would include a custom theme using mat-light-theme(..) or mat-dark-theme(..), we now use my-light-theme(..) and my-dark-theme(..).

At this point we will be be able to use this theme palette in our own components. These components can now easily be re-themed.

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

@mixin awesome-component-theme($theme) {
  $success: map-get($theme, success);
  $success-color: mat-color($success);
  $success-contrast: mat-color($success, default-contrast);

  .app-awesome {
    background: $success-color;
    color: $success-contrast;
  }
}

What about Material components?

This is well and good, we can use our new theme palette in our own components. But how can we use this in Angular Material components?

Lets add this to the MatButton component.

First we need to override the _mat-button-theme-property mixin to add the mat-success classes.

@mixin _mat-button-theme-property($theme, $property, $hue) {
  $primary: map-get($theme, primary);
  $accent: map-get($theme, accent);
  $warn: map-get($theme, warn);
  $success: map-get($theme, success);
  $background: map-get($theme, background);
  $foreground: map-get($theme, foreground);

  &.mat-primary {
    #{$property}: mat-color($primary, $hue);
  }
  &.mat-accent {
    #{$property}: mat-color($accent, $hue);
  }
  &.mat-warn {
    #{$property}: mat-color($warn, $hue);
  }
  &.mat-success {
    #{$property}: mat-color($success, $hue);
  }

  &.mat-primary, &.mat-accent, &.mat-warn, &.mat-success, &[disabled] {
    &[disabled] {
      $palette: if($property == 'color', $foreground, $background);
      #{$property}: mat-color($palette, disabled-button);
    }
  }
}

Now we cannot simply use:

<button mat-button color="success">SUCCESS</button>

This will work ok, until it doesn't.

It only works because of regression bug in Angular that fails to type check string literals. This will be fixed in the future. This means that we can try to use the mat-button color="success" to apply our new color to the Angular Material components -- but it only works because of a bug. So let's not do this.

Using a CSS Class

One solution would be to simply add a class to the element:

<button mat-button class="mat-success">SUCCESS</button>

Using a Directive to Extend Angular Material

Or we can create a directive to do this for us.

type ColorClasses = 'mat-primary' | 'mat-accent' | 'mat-warn' | 'mat-success' | undefined;

@Directive({
  selector: '[myColor]'
})
export class MyColorDirective {

  @Input() set myColor(value: ColorClasses) {
    this.renderer.addClass(this.element.nativeElement, `mat-${value}`);
  }

  constructor(private element: ElementRef, private renderer: Renderer2) { }
}
<button mat-button myColor="success">SUCCESS</button>

When the regression bug that is preventing string literals from being type checked in templates is fixed, this approach will improve developer experience.

Conclusion

Angular Material is powerful, though it may seem too prescriptive at times. That is until you look into the internals of the SCSS themes, typography and see there are many extension points.

One of the strengths of Angular is being able to extend using attribute directives. Adding behaviours and palettes using directives is simply the least obtrusive method for extension.

Discussion

pic
Editor guide
Collapse
pedy711 profile image
pedy711

nice article, I want to change the color of "mat-icon" by overriding "$mat-light-theme-foreground" variable inside "_theming.scss", but after changing the value for "icon" or "icons" nothing happen. But when I change the "text" value, then all texts and icons colors change which is not intended. I just need to change the icon color. Like below:

$mat-light-theme-foreground {
icon: green,
icons: green,
}

Collapse
vikramkadiam profile image
Vikram Kadiam

This is nice !

Collapse
martinmcwhorter profile image
Martin McWhorter Author

Feel free to repost this everywhere! 😊