DEV Community

loading...

Angular 6: Dynamic Themes Without a Library

adamaso profile image Angela Damaso ・3 min read

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.

tumblr

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:

  1. 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.

  2. 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!

implementation

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!

Discussion (15)

pic
Editor guide
Collapse
pcthien profile image
pcthien

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;
}

Collapse
briancodes profile image
Brian • Edited

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);, the var(--navBackground) SCSS function is returning var(--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 bit

Collapse
adamaso profile image
Angela Damaso Author

Hi 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 :)

Collapse
danquack profile image
Daniel Quackenbush

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!

Collapse
demonarxs1 profile image
Dmitry

Hey. Thanks for the code. But I have a question. Why are you using '$ variable' if it works without it too?

Collapse
aayushregmi profile image
aayushregmi

This article has been very helpful. But I had one question how can you handle darken and lighten function with css variables?

Collapse
maximrobota profile image
Maxim • Edited

Very important question !!!
$variables: (
--cardColor: darken(var(--cardColor), 50%),

argument $color of darken($color, $amount) must be a color

Collapse
irisngu43545715 profile image
Iris Nguyen

This cannot work on IE 11. Do you have any idea :)

Collapse
donqq profile image
Don Dilanga

CSS variables don't work with IE 11.
caniuse.com/#feat=css-variables

Collapse
eugenekmd profile image
Eugene K

what about polyfill for css variables
github.com/jhildenbiddle/css-vars-...

Collapse
nestorperez13 profile image
NestorPerez13

Very useful, thank you. For noobs like me you forgot to tell that the parent component should have the class theme-wrapper.

Collapse
brandonjerz profile image
Brandon Jerz

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

Collapse
tackgnol profile image
Adam Kościelniak

Any idea on getting it to work on IE 11 and Edge?

Collapse
kennross profile image
kenneross

This is awesome!

Collapse
willwalker profile image
Walker Sousa

Need this slashes before? I can't use without slashes, why?