Build Truly Dynamic Theme with CSS Variables

Siyang Kern Zhao
Aug 1, 2019 · 6 min read

Change theme during run time. Work with Angular Material.

Photo by Siora Photography on Unsplash

Let’s see the first challenge

Final Result: Change theme in the fly

The above gif is the challenge we are going to conquer. Main tasks are:

  1. Have two squares, one reflecting primary color, the other reflecting secondary color.
  2. Title text “angular-dynamic-theme-example” has primary color, and secondary color on hover.
  3. Have two inputs and one save button to dynamically change the two colors.

At first glance, you may think it is an easy task and already have your solution(s) in mind.

Possible solution: Put color variables in JS and bind with ngStyle

  1. Have two JS variables primaryColor and secondaryColor and update the two variables when save button is hit.
  2. Bind the two variables to the template through ngStyle

  1. Colors can’t be applied to pseudo class or pseudo element (such as h1::before div::after p::first-line a:active input:disabled button:hover etc.), so task 2 can’t be done.
  2. Probably not work well with your design systems. If you have your design system implemented with scss variables like $primary-color and $secondary-color and they are applied everywhere. You will have to deprecate them and do a big refactoring because scss variables are built during compile time by replacing the variables with real value.

CSS Variables to the Rescue

CSS variables is a standard which enables you to have variables in CSS.

:root {
--first-color: #488cff;
--second-color: #ffff8c;
} // we can also define variables with JavaScript

div {
background-color: var(--first-color);
color: var(--second-color);
}

SCSS is compiled to CSS during compile time, and SCSS variables are replaced with resolved value during compile time, which means there is no way to change the variable during run time. However, CSS variables just sits there during run time, and you can dynamically CRUD them

Solution Code

// app.component.ts@Component({
selector: 'app-root',
template: `
<h1>angular-dynamic-theme-example</h1>

<div class="block primary-background">Primary</div> <br>
<div class="block secondary-background">Secondary</div> <br>

Set Primary Color:
<input type="text" [(ngModel)]="primaryColor"> <br>

Set Secondary Color:
<input type="text" [(ngModel)]="secondaryColor"> <br>

<button (click)="changeTheme(primaryColor, secondaryColor)">Save</button>
`,
styleUrls: ['./app.component.scss']
})
AppComponent {
primaryColor: ;
secondaryColor: ;

() {
.changeTheme('red', 'yellow'); // Set default theme
}

changeTheme(primary: , secondary: ) {
document.documentElement.style.setProperty('--primary-color', primary);
document.documentElement.style.setProperty('--secondary-color', secondary);
}
}

As you can see, we can use Web API document.documentElement.style.setProperty to set CSS variables.

// app.component.scss$primary: var(--primary-color);
$secondary: var(--secondary-color);

h1 {
color: $primary;
}
h1:hover {
color: $secondary;
}

.block {
width: 100px;
height: 100px;
}

.primary-background {
background-color: $primary;
}

.secondary-background {
background-color: $secondary;
}

Dynamically change theme of Angular Material

Instead of using one single color for primary color, Angular Material asks for a color palette for primary color when we customize theme. Basically a color palette looks like this:

$dark-primary-text: rgba(black, 0.87);
$light-primary-text: white;
$mat-red: (
50: #ffebee,
100: #ffcdd2,
200: #ef9a9a,
300: #e57373,
400: #ef5350,
500: #f44336,
600: #e53935,
700: #d32f2f,
800: #c62828,
900: #b71c1c,
A100: #ff8a80,
A200: #ff5252,
A400: #ff1744,
A700: #d50000,
contrast: (
50: $dark-primary-text,
100: $dark-primary-text,
200: $dark-primary-text,
300: $dark-primary-text,
400: $dark-primary-text,
500: $light-primary-text,
600: $light-primary-text,
700: $light-primary-text,
800: $light-primary-text,
900: $light-primary-text,
A100: $dark-primary-text,
A200: $light-primary-text,
A400: $light-primary-text,
A700: $light-primary-text,
)
);

In order to change theme dynamically, we would need to provide a palette consisting of 28 color variants. So let’s create 28 CSS variables like this:

$dynamic-theme-primary: (
50 : var(--theme-primary-50),
100 : var(--theme-primary-100),
200 : var(--theme-primary-200),
300 : var(--theme-primary-300),
400 : var(--theme-primary-400),
500 : var(--theme-primary-500),
600 : var(--theme-primary-600),
700 : var(--theme-primary-700),
800 : var(--theme-primary-800),
900 : var(--theme-primary-900),
A100 : var(--theme-primary-A100),
A200 : var(--theme-primary-A200),
A400 : var(--theme-primary-A400),
A700 : var(--theme-primary-A700),
contrast: (
50: var(--theme-primary-contrast-50),
100: var(--theme-primary-contrast-100),
200: var(--theme-primary-contrast-200),
300: var(--theme-primary-contrast-300),
400: var(--theme-primary-contrast-400),
500: var(--theme-primary-contrast-500),
600: var(--theme-primary-contrast-600),
700: var(--theme-primary-contrast-700),
800: var(--theme-primary-contrast-800),
900: var(--theme-primary-contrast-900),
A100: var(--theme-primary-contrast-A100),
A200: var(--theme-primary-contrast-A200),
A400: var(--theme-primary-contrast-A400),
A700: var(--theme-primary-contrast-A700),
)
);

Then we just need to set these 28 variables with JavaScript. For demo purpose, instead of asking for 28 colors by filling out 28 text inputs, I leverage the source code (with modification) of this palette generator, which basically asks for one base color, and automatically generate the 28 colors for you. And we just need to document.documentElement.style.setProperty 28 times to dynamically set those 28 CSS variables:

savePrimaryColor() {
.primaryColorPalette = computeColors(.primaryColor);

(color .primaryColorPalette) {
key1 = `--theme-primary-${color.name}`;
value1 = color.hex;
key2 = `--theme-primary-contrast-${color.name}`;
value2 = color.darkContrast ? 'rgba(black, 0.87)' : 'white';
document.documentElement.style.setProperty(key1, value1);
document.documentElement.style.setProperty(key2, value2);
}
}

.

However, there is a bug…

Ripple effect doesn’t have opacity

The ripple effect doesn’t look right. It’s not supposed to be solid white.

I took a look at the Angular Material source code here, and spot the problem.

The problem is that inside mat-color function, the $color refers to a CSS variable. type-of($color) != color evaluates to true and it just return the color without adding any opacity.

Fix the bug

My Pull Request for fixing this bug was merged into Angular material so you don’t need to take any action

This is how I fix the bug by modifying the source code by explicitly adding opacity if mat-color returns a CSS variable (not a color value).

Ripple effect corrected

As this may be a small improvement of the Angular Material library, I would try my luck by submitting a pull request. 🤞

Summary

  1. CSS itself has its variable mechanism to enable you write CSS value by reference.
  2. CSS variable is just there during run time while Sass variable is replaced by resolved value during compile time.
  3. CSS variable can be dynamically CRUD’ed during run time with JavaScript Web API. It can also be defined initially in CSS/SCSS

  1. CSS variable is not natively supported by IE 😐(I know right?), but polyfill is available.
  2. You can set fallback value in case the variable is not defined.
.header {
color: var(--header-color, blue);
/* if header-color isn’t set, fall back to blue*/
}

3. CSS variable can be directly referred by SASS variable, but SASS variable cannot be directly referred by CSS variable. e.g.

// RIGHT, CSS variable can be directly referred by SASS variable
$primary-color: var(--my-primary-color);
$accent-color: #fbbc04;

{
// WRONG, will not work in recent Sass versions.
--accent-color-wrong: $accent-color;

// RIGHT, will work in all Sass versions.
--accent-color-right: #{$accent-color};
}

Last but not least, big thanks to peer reviewers Wojciech Trawiński, Nicholas Jamieson and Max Koretskyi aka Wizard who have gave great advice to this article.

Angular In Depth

The place where advanced Angular concepts are explained

Siyang Kern Zhao

Written by

Independent Contractor, Angular/NodeJS Developer

Angular In Depth

The place where advanced Angular concepts are explained

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade