Coding a CSS Theme Switcher – a Multitude of Web Dev Options

  • infoFull Post Details
    info_outlineClick for Full Post Details
    Date Posted:
    Aug. 16, 2019
    Last Updated:
    Aug. 19, 2019
  • classTags
    classClick for Tags

Table of Contents:

Introduction:

Front-end web theming seems to be a current hot topic, and I have to admit that I felt the itch to jump on the bandwagon and add a dark theme to a Single-Page Application I’m working on. But where to get started? There are so many guides out there, and yet I couldn’t find many that I felt adequately talked about CSS theming in a generic way that covers many different approaches, rather than a tutorial that is essentially “copy and paste this into your code”.

So I set out to make one (this post), a guide that briefly discusses each of the many available options in a broad manner, with some short snippets of example code.

First though…

What is theming?

In this context, CSS theming or CSS theme switching refers to a set of shared styles (colors, etc) that are grouped as a theme, and being able to switch between themes instantly on a webpage or within a SPA, without refreshing the page.

A very simple example of a theme, that I use throughout this post, is as follows:

  • Dark theme:
    • Primary Color: Black
    • Secondary Color: White
  • Light Theme:
    • Primary Color White
    • Secondary Color: Black

Ok, now lets look at different options for implementation:

Method A: Using CSS custom properties / variables

This is fast becoming one of the most common current approach to implementing switchable themes, and uses a newer CSS feature called “Custom Properties“, which are often referred to as “CSS variables”. In short, CSS custom properties allow you to define a variable, such as “primaryColor”, which can then be referenced in hundreds of other spots throughout your CSS.

This is crucial for theming, because it means that you can share a property among thousands of elements, and to change it, you just need to update that one variable value, rather than needing to individually update each element’s CSS.

To get into the details of how to use CSS variables to deliver switchable CSS themes, let’s break this method into a few different approaches:

Method A-1: Using specificity to change variable inheritance

Ok, long title, but I promise you that this method is actually pretty simple. In fact, this is one of the most common methods, and can be seen in current use across several *major* websites. Basically, you define the default theme as a set of variables, and then override the exact same variables through another block of CSS. The override will work because we will add something specific (class or attribute) to a top level node (like <body>) to trigger the more specific rule.

Here is some example CSS, without using custom properties:

body .myElement {
    background-color: white;
}
body.dark .myElement, body[data-theme="dark"] .myElement {
    background-color: black;
}

And here is the same output, but using CSS variables:

body {
    --primaryColor: white;
    --secondaryColor: black;
}
body .myElement {
    background-color: var(--primaryColor);
}
body.dark, body[data-theme="dark"] {
    --primaryColor: black;
    --secondaryColor: white;
}

Then you can simply toggle the class or attribute with JS, either directly with something like `.setAttribute(‘data-theme’,’dark’)`, or with a reactive binding within something like React or Vue.

Here is a quick demo using setAttribute on <body> to switch themes:

You could technically do this without using CSS custom variables, but you end up with a lot of handcoding and repeating rules. For every rule/element that has different styling for a specific theme, you only have to write that block once if you are using variables, but if not, you have to write it 1 x as many themes as you have. Example below, where you can see that with three elements and three themes, the minimum number of CSS blocks without custom properties is 9 (3×3), whereas with variables, we only need 3 (one for each element):

Method A-2: Re-assign variable values with JS

This method usually is used when you aren’t just theme switching, but also allowing for live theme property changing by the user, through something like a color picker. Let’s pretend that the user has just picked a new custom primary color for their theme, and they want to preview how it will look. Here is the initial CSS:
:root {
    --primaryColor: red;
    --secondaryColor: blue;
}

With just a little bit of JavaScript, we can easy modify any of these values. Here we go changing the primary color:

// This will target :root variables
document.querySelector(':root').style.setProperty('--primaryColor',newColor);
// Or, same thing...
document.documentElement.style.setProperty('--primaryColor',newColor);

You can take this a step further and easily implement theming logic to swap out entire themes and switch between multiple:

const themes = {
    light: {
        '--primaryColor': 'white',
        '--secondaryColor': 'black'
    },
    dark: {
        '--primaryColor': 'black',
        '--secondaryColor': 'white'
    },
	red: {
		'--primaryColor': 'red',
		'--secondaryColor': 'white'
	}
}
function activateTheme(theme){
    for (let prop in theme){
        document.querySelector(':root').style.setProperty(prop, theme[prop]);
    }
}
// Switch to the dark theme:
activateTheme(themes.dark);

Demo:

By default, when you change a CSS property via Javascript, the change is not persisted when the user reloads the page. To save their changes to a theme, you have a bunch of different options, which is beyond the scope of this post. However, here are some generic options:

    • On value change, also AJAX to server and save with User data
      • Then, when user logs in, send back theme data either as on-the-fly generated CSS, or JSON which gets parsed and mapped back to CSS custom vars
    • Persist theme to localStorage and map back on page load
      • If you are using a state management system and storing the user’s theme config there, this should be very easy to do with something like “redux-persist”
      • You can also just inject a <style> tag into the page with overriding variable values; this is how dev.to is currently handling theming – on page load, it injects a different set of variable values through a <style> tag, depending on the value of a localStorage value

Method B: Using SASS (Sass/Scss)

Technically, you could use Sass to accomplish both methods A & B, since Sass transpiles to CSS, and you could also just stick both methods, as CSS, into Sass, and it should be valid.

However, there are some Sass specific approaches for theme switching, which I’ve outlined below.

Sidenote: Throughout this post, my demos all use the SCSS style of Sass, but all of this should be possible with indented SASS; you would just need to reformat.

Method B-1: Themes as Nested Maps

One way to approach this in Sass is with nested maps. The inspiration for this code, and one of the best guides on this method is this post.

Nested maps in Sass resemble JSON, and allow nesting of key-pair maps that can hold any valid Sass value. This makes them a great way to hold multiple named themes, and sub-properties for each theme. Here are my two example themes as a nested map saved to $themes:

$themes: (
    'dark': (
        'primary': black,
        'secondary': white
    ),
    'light': (
        'primary': white,
        'secondary': black
    )
);

This by itself does not generate any CSS, so you have to write your own function/code to loop through your themes and generate the CSS you want. It is best to define re-usable helper functions to handle this, that use mixins:

/**
* Mixin to use to generate blocks for each theme
* Automatically takes @content
*/
$scopedTheme: null;
@mixin themeGen($allThemesMap: $themes) {
    @each $themeName, $themeMap in $allThemesMap {
        .theme-#{$themeName} & {
            // Creating a map that contains values specific to theme.
            // Global is necessary since in mixin
            $scopedTheme: () !global;
            @each $variableName, $variableValue in $themeMap {
                // Merge each key-value pair into the theme specific map
                $scopedTheme: map-merge($scopedTheme, ($variableName: $variableValue)) !global;
            }
            // The original content passed
            @content;
            // Unset
            $scopedTheme: null !global;
        }
    }
}
/**
* Function to call within themeGen mixin, to get value from the current theme in the iterator
*/
@function getThemeVal($themeVar){
    @return map-get($scopedTheme,$themeVar);
}

Finally, using this with an element:

/**
* Actually using theme values to generate CSS
*/
.myComponent {
    @include themeGen() {
        background-color: getThemeVal('primary');
        color: getThemeVal('secondary');
    }
}

Here is a demo showing the final CSS this can generate:

Technically, you could hand code the CSS that this generates (and some old-school devs do), but there is not a good reason to do so, and if anything, you are more likely to write error-ridden CSS.

A related, but slightly different approach (avoids global vars) can be found here. And here’s another variation.

Method B-2: Simple Mixin “Dump”

This method is not as elegant as C-1, but less complicated. Essentially you dump a huge block of CSS into a mixin, which then repeats it, but with a named parent theme class.

@mixin scopeToTheme($themeName,$prop,$val){
    .#{$themeName} & {
        #{$prop}: #{$val}
    }
}

button {
  background-color: red;
  padding: 20px;
  margin-bottom: 4px;
  border-radius: 10px;
  @include scopeToTheme(
    'dark-theme',
    'background-color',
    'black'
  );
  @include scopeToTheme(
    'light-theme',
    'background-color',
    'white'
  )
}

And here is the CSS that this generated:

button {
  background-color: red;
  padding: 20px;
  margin-bottom: 4px;
  border-radius: 10px;
}
.dark-theme button {
  background-color: black;
}
.light-theme button {
  background-color: white;
}

Method B-3: CSS Custom Properties to Sass

I’m not sure I’ve seen this done anywhere, but an interesting approach could be to combine Method B with Sass by *pulling* the CSS variables into Sass. Like so – CSS:

:root {
    --primaryColor: red;
    --secondaryColor: blue;
}

Sass:

$primaryColor: var(--primaryColor);
$secondaryColor: var(--secondaryColor);

Method C: Mutating State

I won’t get into the specific of how to code this out, but this is pretty easy to think about conceptually. In a reactive framework, you can hold CSS properties, such as colors, as part of `state`. Then, using a CSS-in-JS framework, or even just sticking state in inline css, you can plug the value from state into your scoped CSS.

It looks like Twitter uses this method, with React Native Web.

Method D: Crazy Old-School

There are so many reasons not to do it this way, but I feel obligated to share this hackish idea anyways. If you really wanted to insist on using CSS, but not using any variables or touching of the `<body>` class or attributes, you could hand code different themes as completely separate CSS files. Like `dark-theme.css` and `light-theme.css`. Then, to switch the theme, you could use AJAX to pull in the new theme file. Or, if you wanted to support browsers without Javascript, you could even use server side code to change which theme gets included and force a page refresh.

As a side note, an effect of this approach is actually better utilization of bandwidth, since you are only sending what theme is needed.

Wrap-Up / Final Thoughts:

First, although this was an extensive look at theming options, it should not be considered fully comprehensive; there is an endless number of ways you can implement custom theming.

Second, if you have made it all the way through the post, you might still be wondering “well, which option is the best one?”. The truth is that it really matters what your objective is. If your goal is to support as many browsers as possible and/or have functions within styling (like converting hex to RGBA and then darkening), then SASS is probably the way forward (Method B). Or, if you want to have the smallest CSS filesize and/or binding of CSS values to JS values, then CSS custom properties are likely the best fit (Method A and Method C, or Method B with special setup).

The short answer is use what fits best with the framework or existing structure of your site/SPA/App. And have fun theming!

Leave a Reply

Your email address will not be published. Required fields are marked *