Light mode/Dark mode: Dynamic theming through SCSS mixin

David Xu
5 min readMar 25, 2022

--

So you want to build add a theming system to your web app? Then you are in the right place. In this article, I will show you how to accomplish this with the help of a simple SCSS mixin. Let’s get started.

Initial Attempt: Flow Control

When building a theming system in SCSS, your first instinct might be to do this with flow control. For example:

$dark-theme: false !default;
$bg: #ffffff !default;
$text: #000000 !default;
$brand: #456789 !default;
@if $dark-theme {
$bg: #111111;
$text: #eeeeee;
$brand: #abcdef
}
p {
color: $text;
background-color: $bg;
}
button {
background-color: $brand;
color: $text;
}
a {
color: $brand;
}

And this works. When the variable dark-theme is true, the button’s background colour will be set to#333333 . When dark-theme is false, the button’s background colour will be set to #ffffff .

The Problem

The problem is that since SCSS is precompiled, the above code would get compiled into:

p {
color: #000000;
background-color: #ffffff;
}
button {
background-color: #456789;
color: #000000;
}
a {
color: #456789;
}

As you can see, the $dark-theme variable no longer exists, and the values for dark mode is also gone. Basically, after the build process, the theme is baked in, and it’s impossible for the client-side code to change the theme on the fly.

New Approach

We want the compiled CSS to contain the colours all the elements should have during both light mode AND dark mode. We could distinguish between the themes based on a class name assigned at the root of the DOM tree. For example, the HTML might look something like this:

<div className="dark-theme">
<p>Paragraph</p>
<button>Button</button>
<a href="/">Anchor</a>
</div>

And the CSS would look something like this:

.light-theme p {
color: #000000;
background-color: #ffffff;
}
.light-theme button {
background-color: #456789;
color: #000000;
}
.light-theme a {
color: #456789;
}
.dark-theme p {
color: #eeeeee;
background-color: #111111;
}
.dark-theme button {
background-color: #abcdef;
color: #eeeeee;
}
.dark-theme a {
color: #abcdef;
}

Of course, we don’t want to write this out manually. It would be tedious to try and keep track of changes across two sets of CSS for each tag. Not to mention things could be even more complicated if we’ve got 3, 4 or even 5 themes! (Holiday specific themes anyone?)

That’s why we need to set up a mixin to accomplish this for us.

The Solution

First of all, you want to declare all your themes in a $themes variable like so:

$themes: (
light: (
text: #333333,
bg: #ffffff,
brand: #567567,
),
dark: (
text: #ffffff,
bg: #333333,
brand: #abcabc,
),
);

Next, we are going to set up our themify mixins

@mixin themify($themes) {
@each $name, $values in $themes {
.#{$name}-theme {
$theme-map: $values !global;
@content;
}
}
}
@function themed($key) {
@return map-get($theme-map, $key);
}

Essentially, what this is doing is it will loop through the $themes map, and for each theme, generate all your styles for all your components and prefix it with a .light-theme or .dark-theme , or whatever else you’ve chosen to name your themes in the $themes variable .

After you have declared your mixins, you will be able to use them like so:

@include themify($themes) {
p {
color: themed("text");
background-color: themed("bg");
}
button {
background-color: themed("brand");
color: themed("text");
}
a {
color: themed("brand");
}
} ;

With this approach, even if you’ve got 20 themes, you will only need to write them out for each component once, and the mixin will be responsible for generating all 20 variations of each component based on your $themes colours.

And that’s it. Now all you need is some kind of javascript to change the top-level element’s class name from theme-light to theme-dark and your entire app with a changing theme instantly.

Last words

This setup should be good enough to get you through the most basic theming needs. Once you understand the mixin, it’s fairly easy to use and low maintenance.

There is, however, one caveat to be careful of when using this mixin. Because that the themifymixin will regenerate its content n number of times, where n is the number of themes you’ve got, there’s a potential it will unnecessarily bloat your stylesheet, leading to slow initial load time for your app.

For example, consider the following style declaration:

/* input.scss */@include themify($themes) {
p {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
border-width: 10px;
border-radius: 10px;
width: 50px;
height: 50px;
transition: smooth;
color: themed('text');
background-color: themed('bg');
}
};

After compilation what you would end up with is a lot of duplications, like so:

/* output.css */.light-theme p {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
border-width: 10px;
border-radius: 10px;
width: 50px;
height: 50px;
transition: smooth;
color: #000000;
background-color: #ffffff;
}
.dark-theme p {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
border-width: 10px;
border-radius: 10px;
width: 50px;
height: 50px;
transition: smooth;
color: #eeeeee;
background-color: #111111;
}

So instead, you want to only wrap the properties you are styling in the mixin like this:

/* input.scss */p {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
border-width: 10px;
border-radius: 10px;
width: 50px;
height: 50px;
transition: smooth;
}
@include themify($themes) {
p {
color: themed('text');
background-color: themed('bg');
}
};

That way, the compiled CSS will be much shorter:

/* output.css */p {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
border-width: 10px;
border-radius: 10px;
width: 50px;
height: 50px;
transition: smooth;
}
.light-theme p {
color: #000000;
background-color: #ffffff;
}
.dark-theme p {
color: #eeeeee;
background-color: #111111;
}

And that’s it. Now go and set up that sweet, beautiful dark mode on your app.

--

--