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
- Add palettes to a theme
- 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 success
palette 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.
Top comments (5)
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,
}
This is nice !
Feel free to repost this everywhere! 😊
Hi Martin, i try to apply your code in my app, with angular-core 14.2.0 and material 14.2.7 but doesn't work.
It could be due to the angular/material version?
Thanks!
Awesome article!