The concept of theming has been around as long as I can remember. Giving users the ability to choose the look and feel of your product has incredible value — it creates a more localized experience and reduces developer maintenance time.
How can we create something like this in our Angular apps?
Why Sass Alone Won’t Work
Although Sass variables may work to create a preset themed experience, the biggest drawback is that it can’t be manipulated by JavaScript. We need JavaScript to dynamically change the value of our variable!
Why Material Alone Won’t Work
Ever since Angular Material was released, developers have been flocking to this library to utilize their reusable components (not to mention built-in accessibility)
Material comes with built-in theming, but this may not work for two reasons:
By default, Material comes with it’s own color palette that is optimized for accessibility. If you want to generate more colors, you’ll need to pass it into their mat-palette mixin or create a new theme file, using 3rd party tooling. This creates an external dependency and restricts the ability to switch themes without touching code.
Although it is a great option, not everyone wants to use Material! Many developers do not wish to import an entire library to utilize components and opt to create their own.
The solution? Sass + CSS Variables!
If you have never used native CSS Custom Properties (I call them variables), there is a great article (here) to help you get started. The reason this approach works is because CSS variables can manipulated by JavaScript! With this combination, you can use a form to pass CSS variables to a Sass map, which can be used throughout the app.
Let’s See It!
This implementation:
- Does not use any external libraries
- Allows multiple components to dynamically change styles through a form
- Saves the form as an object that can be saved in a database or local store
- Has the capability to load an external object as a preloaded or preset style
Link to Demo: https://native-theming-form-medium.stackblitz.io/
Link to Stackblitz: https://stackblitz.com/edit/native-theming-form-medium
The Magic
The core principle behind this method is combining Sass maps and CSS Variables.
In thetheme.scss file, the default values are set and passed into a Sass map
theme.scss
// default colors
.theme-wrapper {
--cardColor: #CCC;
--cardBackground: #FFF;
--buttonColor: #FFF;
--buttonBackground: #FFF;
--navColor: #FFF;
--navBackground: #FFF;
--footerColor: #FFF;
--footerBackground: #FFF;
--footerAlignment: left;
}
// pass variables into a sass map
$variables: (
--cardColor: var(--cardColor),
--cardBackground: var(--cardBackground),
--buttonColor: var(--buttonColor),
--buttonBackground: var(--buttonBackground),
--navColor: var(--navColor),
--navBackground: var(--navBackground),
--footerColor: var(--footerColor),
--footerBackground: var(--footerBackground),
--footerAlignment: var(--footerAlignment)
);
A function is created to return the native css variable from the global sass map
function.scss
@function var($variable) {
@return map-get($variables, $variable);
}
The components can now read these two files to host a dynamic variable that changes upon form resubmit
card.component.scss
@import '../../theme';
@import '../../functions';
.card {
background-color: var(--cardBackground);
color: var(--cardColor);
}
The card’s background color is now #FFFFFF and text color is #CCCCCC
But how do we change the values?
Through the theme-picker component!
In our theme-picker.component.html file, we are using template forms with ngModel to create an object with a unique key (style) and value (input). The object then gets passed to the TypeScript file which dynamically overwrites the variable.
theme-picker.component.ts
// searching the entire page for css variables
private themeWrapper = document.querySelector('body');
onSubmit(form) {
this.globalOverride(form.value);
}
globalOverride(stylesheet) {
if (stylesheet.globalNavColor) {
this.themeWrapper.style.setProperty('--navColor', stylesheet.globalNavColor);
}
...
if (stylesheet.globalButtonColor) {
this.themeWrapper.style.setProperty('--buttonColor', stylesheet.globalButtonColor);
}
}
The globalOverride function checks to see if a value exists for that specific variable, then replaces each CSS Variable with the new inputted one.
Violá!
This code can be better optimized to scale (using preset style objects, saving/publishing styles on submit), so feel free to play around with it!
Top comments (15)
Thanks for your article,
Can you please help with my small concern?
Your default colors seem not working. I tried to change the value of them, but nothing happen.
// default colors <------ These one
.theme-wrapper {
--cardColor: #CCC;
--cardBackground: #FFF;
--buttonColor: #FFF;
--buttonBackground: #FFF;
--navColor: #FFF;
--navBackground: #FFF;
--footerColor: #FFF;
--footerBackground: #FFF;
--footerAlignment: left;
}
This is a very good approach - I had a look at
caniuse
and css variables seem to be very well supported now, which is great! It's really helpful seeing a live demo too on StackBlitz ⚡I have a question about the SCSS
var
funciton. When we are setting a value like:background-color: var(--navBackground);
, thevar(--navBackground)
SCSS function is returningvar(--navBackground)
. Is this step necessary do you think? If the colors are mapped 1 to 1, we might be able to skip this step? I might be missing something with this bitHi Brian, sorry for getting back so late to your comment! YES, var can be omitted, I am actually planning to update this article with an updated implementation that is a bit cleaner. Thanks for reading :)
FYI - I recently implemented this into my Angular 5 project. Great article, just wanted to share that it’s not just limited to Angular 6!
Hey. Thanks for the code. But I have a question. Why are you using '$ variable' if it works without it too?
This article has been very helpful. But I had one question how can you handle darken and lighten function with css variables?
Very important question !!!
$variables: (
--cardColor: darken(var(--cardColor), 50%),
argument
$color
ofdarken($color, $amount)
must be a colorThis cannot work on IE 11. Do you have any idea :)
CSS variables don't work with IE 11.
caniuse.com/#feat=css-variables
what about polyfill for css variables
github.com/jhildenbiddle/css-vars-...
Very useful, thank you. For noobs like me you forgot to tell that the parent component should have the class theme-wrapper.
I think you could even do this without sass. Just update the css variables using the setProperty function in ts. Oninit you can dynamically set the css variables to the users saved theme
Any idea on getting it to work on IE 11 and Edge?
This is awesome!