Dynamically Change Angular Material Theme Colors
Have you ever wanted to make a website where users have control over the colors of the site? Maybe it’s a B2B app, and the businesses that you sell to want to see their own brand colors. Recently, I ran into this when building a trivia game app to be played at weddings. Many people choose “wedding colors” and want to see those colors reflected in all parts of the day.
I could easily find examples of how to switch between various pre-configured Angular themes, but I didn’t want to build out specific themes to make the user choose between. I wanted them to be able to use literally any primary and accent color of their choosing.
I had trouble finding examples of making the theme truly flexible and dynamic like that. I eventually figured it out, and this tutorial will walk through how to do it.
To reiterate: this will make it so that you can change the theme colors of your Angular app to any colors without making code changes!
Creating a new Angular app and running it locally
Install the Angular CLI:
npm install -g @angular/cli
Create a new Angular app:
ng new my-demo-app
Choose SCSS for the stylesheet format.
Say No when it asks about Server-Side Rendering.
Once it finishes installing everything, open the new folder in your editor of choice. If you’re using VSCode, you can do that with the commands:
cd my-demo-app
code .
Start the app with:
ng serve
In your browser, go to http://localhost:4200. You will see the Angular app!
In your editor, open app.component.html. Make some change to the file, like adding a new line of:
<p>Editing works!</p>
Save the file, and go back to the tab in your browser. You should see your change show up!
Installing Angular Material
Add Angular Material to your app with this command:
ng add @angular/material
When it asks you if your would like to proceed with installing the @angular/material package, say Yes.
Choose Indigo/Pink as the prebuilt theme name.
Choose any option when it asks about adding animations; it’s not relevant to this.
Using Angular Material components to see theme colors
Replace the entire contents of app.component.html
with this:
<button mat-raised-button color="primary">Primary Button</button>
<button mat-raised-button color="accent">Accent Button</button>
Replace the entire contents of app.component.ts
with this:
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
@Component({
selector: 'app-root',
standalone: true,
imports: [MatButtonModule],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {}
You will now see two buttons on your page with the Angular Material styling. One will be indigo (the primary color), and the other will be pink (the accent color).
Setting up dynamic theme colors
Now we’re ready to stop using the default Indigo/Pink theme, and instead make the theme colors fully customizable.
At the top level of the src
folder, add a new theme.scss
file that looks like this:
@use "@angular/material" as mat;
@include mat.core();
$dynamic-theme-palette: (
50: var(--theme-dynamic-palette-50),
100: var(--theme-dynamic-palette-100),
200: var(--theme-dynamic-palette-200),
300: var(--theme-dynamic-palette-300),
400: var(--theme-dynamic-palette-400),
500: var(--theme-dynamic-palette-500),
600: var(--theme-dynamic-palette-600),
700: var(--theme-dynamic-palette-700),
800: var(--theme-dynamic-palette-800),
900: var(--theme-dynamic-palette-900),
A100: var(--theme-dynamic-palette-A100),
A200: var(--theme-dynamic-palette-A200),
A400: var(--theme-dynamic-palette-A400),
A700: var(--theme-dynamic-palette-A700),
contrast: (
50: var(--theme-dynamic-palette-contrast-50),
100: var(--theme-dynamic-palette-contrast-100),
200: var(--theme-dynamic-palette-contrast-200),
300: var(--theme-dynamic-palette-contrast-300),
400: var(--theme-dynamic-palette-contrast-400),
500: var(--theme-dynamic-palette-contrast-500),
600: var(--theme-dynamic-palette-contrast-600),
700: var(--theme-dynamic-palette-contrast-700),
800: var(--theme-dynamic-palette-contrast-800),
900: var(--theme-dynamic-palette-contrast-900),
A100: var(--theme-dynamic-palette-contrast-A100),
A200: var(--theme-dynamic-palette-contrast-A200),
A400: var(--theme-dynamic-palette-contrast-A400),
A700: var(--theme-dynamic-palette-contrast-A700),
),
);
$dynamic-theme-primary: mat.define-palette($dynamic-theme-palette, 500);
$dynamic-theme-accent: mat.define-palette(
$dynamic-theme-palette,
A200,
A100,
A400
);
$dynamic-theme: mat.define-light-theme(
(
color: (
primary: $dynamic-theme-primary,
accent: $dynamic-theme-accent,
warn: $my-warn,
),
typography: mat.define-typography-config(),
density: 0,
)
);
@include mat.all-component-themes($dynamic-theme);
@include mat.all-component-bases($dynamic-theme);
@include mat.all-component-typographies($dynamic-theme);
@include mat.all-component-densities($dynamic-theme);
In angular.json
, search for @angular/material/prebuilt-themes/indigo-pink.css
. Replace it with src/theme.scss
.
Stop and restart your running local version of the app.
You should now see that both buttons are just black and white. Okay, so our changes did something… but what?
What we’ve done so far is make our app use the theme in theme.scss
, instead of the Indigo/Pink theme. In theme.scss
, we set up the primary and accent theme colors to use CSS variables for the primary and accent colors. But, we have not set the values of those variables anywhere yet. Therefore, we’re just seeing white buttons.
Setting default theme colors
Let’s fix that by first setting default primary and accent colors.
Install the tinycolor2
npm library and its types with these commands:
npm i tinycolor2
npm i --save-dev @types/tinycolor2
Update app.component.ts
, to look like this, and read the comments to understand what it does:
import { Component, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import tinycolor from 'tinycolor2';
interface Color {
name: string;
hex: string;
darkContrast: boolean;
}
@Component({
selector: 'app-root',
standalone: true,
imports: [MatButtonModule],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent implements OnInit {
defaultPrimaryColor = '#4287f5';
defaultAccentColor = '#faa7f7';
ngOnInit(): void {
// On init, set the theme colors to the default colors defined above
this.savePrimaryAndAccentColor();
}
private savePrimaryAndAccentColor(
primaryColor?: string,
accentColor?: string
) {
const dynamicColorPalette = this.computeThemePalette(
primaryColor || this.defaultPrimaryColor,
accentColor || this.defaultAccentColor
);
this.updateThemePalette(dynamicColorPalette);
}
private updateThemePalette(colors: Color[]) {
// Set the values of the CSS variables for each shade of the primary
// and accent theme colors, so that they can be used in theme.scss
// to set the Angular Material theme
colors.forEach((color) => {
document.documentElement.style.setProperty(
`--theme-dynamic-palette-${color.name}`,
color.hex
);
document.documentElement.style.setProperty(
`--theme-dynamic-palette-contrast-${color.name}`,
color.darkContrast ? 'rgba(black, 0.87)' : 'white'
);
});
}
private computeThemePalette(primaryHex: string, accentHex: string): Color[] {
// Uses tinycolor library to get lighter and darker versions of primary
// and accent colors, so that we can make a full theme palette from them
return [
this.getColorObject(tinycolor(primaryHex).lighten(52), '50'),
this.getColorObject(tinycolor(primaryHex).lighten(37), '100'),
this.getColorObject(tinycolor(primaryHex).lighten(26), '200'),
this.getColorObject(tinycolor(primaryHex).lighten(12), '300'),
this.getColorObject(tinycolor(primaryHex).lighten(6), '400'),
this.getColorObject(tinycolor(primaryHex), '500'),
this.getColorObject(tinycolor(primaryHex).darken(6), '600'),
this.getColorObject(tinycolor(primaryHex).darken(12), '700'),
this.getColorObject(tinycolor(primaryHex).darken(18), '800'),
this.getColorObject(tinycolor(primaryHex).darken(24), '900'),
this.getColorObject(tinycolor(accentHex).lighten(50), 'A100'),
this.getColorObject(tinycolor(accentHex), 'A200'),
this.getColorObject(tinycolor(accentHex).darken(20), 'A400'),
this.getColorObject(tinycolor(accentHex).darken(50), 'A700'),
];
}
private getColorObject(value: tinycolor.Instance, name: string): Color {
const c = tinycolor(value);
return {
name: name,
hex: c.toHexString(),
darkContrast: c.isLight(), // determine if the contrast for this color should be black or white
};
}
}
Now, if you save and look at your running app, you should see a blue primary button and a pink secondary button.
Notice that the blue button has white text, while the pink one has black text. This was automatically set based on the darkness of each color, in the getColorObject
function code.
Dynamically changing the colors
Now we’re ready to dynamically change the colors. We’ll add two form inputs, so that you can type the hex code for a primary and an accent color. Then, we’ll add a submit button that changes the theme colors to the form values on click.
Update app.component.html
to look like this:
<button mat-raised-button color="primary">Primary Button</button>
<button mat-raised-button color="accent">Accent Button</button>
<br />
<br />
<form [formGroup]="myForm" (ngSubmit)="onSubmit()">
<mat-form-field appearance="fill">
<mat-label>Primary Color Hex Code</mat-label>
<input matInput formControlName="primaryColor" required />
</mat-form-field>
<br />
<mat-form-field appearance="fill">
<mat-label>Accent Color Hex Code</mat-label>
<input matInput formControlName="accentColor" required />
</mat-form-field>
<br />
<button
mat-raised-button
color="primary"
type="submit"
[disabled]="myForm.invalid"
>
Submit
</button>
</form>
Update app.component.ts
to look like this:
import { Component, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { ReactiveFormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import tinycolor from 'tinycolor2';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
interface Color {
name: string;
hex: string;
darkContrast: boolean;
}
@Component({
selector: 'app-root',
standalone: true,
imports: [
MatButtonModule,
MatFormFieldModule,
ReactiveFormsModule,
MatInputModule,
],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent implements OnInit {
defaultPrimaryColor = '#4287f5';
defaultAccentColor = '#faa7f7';
myForm: FormGroup;
constructor(private fb: FormBuilder) {
this.myForm = this.fb.group({
primaryColor: ['', Validators.required],
accentColor: ['', Validators.required],
});
}
ngOnInit(): void {
this.savePrimaryAndAccentColor();
}
onSubmit() {
// Use form values to set the primary and accent colors in the theme
this.savePrimaryAndAccentColor(
this.myForm.value.primaryColor,
this.myForm.value.accentColor
);
}
private savePrimaryAndAccentColor(
primaryColor?: string,
accentColor?: string
) {
const dynamicColorPalette = this.computeThemePalette(
primaryColor || this.defaultPrimaryColor,
accentColor || this.defaultAccentColor
);
this.updateThemePalette(dynamicColorPalette);
}
private updateThemePalette(colors: Color[]) {
colors.forEach((color) => {
document.documentElement.style.setProperty(
`--theme-dynamic-palette-${color.name}`,
color.hex
);
document.documentElement.style.setProperty(
`--theme-dynamic-palette-contrast-${color.name}`,
color.darkContrast ? 'rgba(black, 0.87)' : 'white'
);
});
}
private computeThemePalette(primaryHex: string, accentHex: string): Color[] {
return [
this.getColorObject(tinycolor(primaryHex).lighten(52), '50'),
this.getColorObject(tinycolor(primaryHex).lighten(37), '100'),
this.getColorObject(tinycolor(primaryHex).lighten(26), '200'),
this.getColorObject(tinycolor(primaryHex).lighten(12), '300'),
this.getColorObject(tinycolor(primaryHex).lighten(6), '400'),
this.getColorObject(tinycolor(primaryHex), '500'),
this.getColorObject(tinycolor(primaryHex).darken(6), '600'),
this.getColorObject(tinycolor(primaryHex).darken(12), '700'),
this.getColorObject(tinycolor(primaryHex).darken(18), '800'),
this.getColorObject(tinycolor(primaryHex).darken(24), '900'),
this.getColorObject(tinycolor(accentHex).lighten(50), 'A100'),
this.getColorObject(tinycolor(accentHex), 'A200'),
this.getColorObject(tinycolor(accentHex).darken(20), 'A400'),
this.getColorObject(tinycolor(accentHex).darken(50), 'A700'),
];
}
private getColorObject(value: tinycolor.Instance, name: string): Color {
const c = tinycolor(value);
return {
name: name,
hex: c.toHexString(),
darkContrast: c.isLight(),
};
}
}
Try it out. You should now be able to enter any hex codes into the inputs, submit, and see all of the buttons change colors appropriately!
What next?
You now have your Angular project set up so that users can dynamically change the theme colors to anything they want!
If you are using this in a real project, you probably want to make a couple updates:
- Add a color picker component, so that users don’t need to look up the hex codes of the colors they want to use.
- Save user color preferences, and load them when they sign in, so that they don’t need to reset the colors every time.
With your knowledge from this tutorial and some general Angular experience, you should be able to easily implement both of those now.
If you have trouble, leave a comment and I can make a Part 2 of this showing how to do that!
Credits: My approach was heavily inspired by this broken StackBlitz project.