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!