Dark Mode Switcher Using CSS Variables in LESS, SASS, or Vanilla CSS

Aditya Mhamunkar
The Startup
Published in
4 min readOct 22, 2020

Are you stuck with (LESS/SASS) pre-processor based color variables and looking around for solutions to implement dark mode?
You came to the right place!

Are you looking for an elegant way to implement a dark mode in your new project?
You came to the right place!

Are you looking for a way to listen to the operating system theme preference and switch your app’s theme accordingly?
You came to the right place!

A complete guide for elegantly implementing and switching between light and dark themes for web apps.

Contents

  • Demo
  • Pre-processor independence
  • Designing color variables in CSS
  • Setting up light and dark themes
  • Listen to the Operating System theme

Demo

Here is a basic vanilla HTML/CSS/JS setup for theming a web app

Try before proceeding

CSS pre-processor independence

A major blocker for theming with color variables is using a pre-processor such as LESS/SASS. Pre-processors’ variables are static, and while pre-processing, the values of these variables get substituted in the classes where they are used. Let’s see how LESS (or SASS) bundles and generates CSS:

// custom-style.less

@primaryColor: #ff0000;

.header-bar {
background-color: @primaryColor;
}

The above file is processed by the LESS pre-processor and generates the following CSS:

// custom-style.css

.header-bar {
background-color: #ff0000;
}

In the generated CSS, we lost the variables and therefore cannot update the colors dynamically.

The solution: CSS properties (a.k.a. CSS variables)

On the other hand, CSS variables (which modern browsers understand) can be updated dynamically.

Now, let’s use CSS variables in LESS:

// custom-style.less:root {
--primary-color: #ff0000;
}
.header-bar {
background-color: var(--primary-color);
}

The above file is processed by the LESS pre-processor and generates the following CSS:

// custom-style.css:root {
--primary-color: #ff0000;
}
.header-bar {
background-color: var(--primary-color);
}

We see that the CSS variables are preserved and thus can be updated dynamically in the browser.

Convert existing LESS/SASS variables to CSS variables

It is easy to convert the existing LESS/SASS variables to CSS variables and update the syntax all over the project.

But, there is one limitation. Pre-processor functions like darken , lighten, fade etc. don’t accept dynamic CSS variables. Such functions require the actual color strings. One way to get around is not to use such functions and instead create a separate CSS variable for the darkened/lightened color.

Designing color variables

When we have light and dark modes, color variables switch values. For example, black can turn white and vice-versa. So, a color variable should not be named with a color name(--black ).

One of the cleaner approaches would be naming the variables according to their purpose. For example --text-color,--page-bg-color,--danger-color,--warning-color, etc.

This makes clear the variable’s purpose so can switch its color value in both modes. For example, --text-color in light mode can be #000000 (black) and in dark mode can be #ffffff (white).

Check the CSS variables used in the demo to see this in action.

UX and Design teams can play an important role in designing the set of color variables to use — try not use random colors or shades of colors for new views.

Setting up light and dark themes

Let’s setup HTML, JS, and CSS.

Setting color variables

:root[data-theme="theme-light"] {
--primary-color: #0072a3;
--text-color: #333333;
--bg-color: #fafafa;
--component-bg-color: #ffffff;
}
:root[data-theme="theme-dark"] {
--primary-color: #49afd9;
--text-color: #e3f5fc;
--bg-color: #1b2a32;
--component-bg-color: #22343c;
}

Having two sets of color variables in the stylesheet can be an overhead. To optimize, switch colors in place using JavaScript — setProperty().

Using color variables

body {
background-color: var(--bg-color)
}
.component {
background-color: var(--component-bg-color);
color: var(--text-color);
border: 1px solid var(--primary-color);
}
h2 {
color: var(--primary-color);
}

Setting up JavaScript

const html = document.querySelector('html');
html.dataset.theme = `theme-light`;
function switchTheme(theme) {
html.dataset.theme = `theme-${theme}`;
}

HTML template

<html>
<body>
<div class="component">
<h2>Theme Switcher</h2>
<button onclick="switchTheme('light')">Light</button>
<button onclick="switchTheme('dark')">Dark</button>
<br /> <br />
<div>
Try changing the theme and start having fun with the dark mode.
</div>
</div>
</body>
</html>

Hurray! Now the app can dynamically switch between light and dark modes.

Now, let’s add a cherry on top!

Listen to the Operating System theme

Your app can also listen to the theme selected in the host Operating System. The MacOS, Windows, and Ubuntu 20+ all support dark/light modes.

The following line of code enables you to check if the dark mode is selected by the operating system.

const isOsDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

The beauty of adding this feature is to listen to the OS level theme change event and switch your app’s theme accordingly. To set up the event listener you can do the following.

const matchMediaPrefDark = window.matchMedia('(prefers-color-scheme: dark)');function startListeningToOSTheme() {
matchMediaPrefDark.addEventListener('change', onSystemThemeChange);
}
function stopListeningToOSTheme() {
matchMediaPrefDark.removeEventListener('change', onSystemThemeChange);
}
function onSystemThemeChange(e) {
const isDark = e.matches;
document.querySelector('html').dataset.theme =
`theme-${isDark ? 'dark' : 'light'}`;
}

Now, you can just add another option called System default along with the Light and Dark the theme, and start listening to the OS theme on selecting System default . Make sure you stop listening when dark or light is selected.

Have fun with the themes!

--

--