A Better Approach to Dark Mode on Your Website

A Better Approach to Dark Mode on Your Website


Dark Mode seems to be here to stay. Initially, mainly used by developers in their code editors, but now it’s all over the place, from mobile, desktops, and now even the web. Supporting Dark Mode is increasingly turning from a nice to have to a must-have. Of course, depending on your niche, but if you are here, chances are, you want to implement Dark Mode on your site or for your clients.

If you check a few tutorials on Google, you’ll soon realize that there are multiple ways to implement dark mode, using HTML attributes, OS settings, JS variables, etc. I’m not here to tell you about all of them, but I’ll tell you the one option I use for my sites and all my customers, including this same website you’re now reading.

Implementing dark mode is a two steps process, first, you need to design both, the light theme and the dark theme for your website, and secondly, you need a way for users to switch themes, either capturing if the user prefers dark or light mode from his/her OS settings or through settings on your own site.

Let’s start discussing the CSS.


Dark Mode Using CSS Trickery

The first option works by the use of “dark” magic 😛, jokes aside, it’s very simple and elegant, and consists of really one line of CSS.

html[data-theme='dark'] {
    background: #000;
    filter: invert(1) hue-rotate(180deg);
}

What? You are probably wondering how that could work, and you may be right to be skeptical, but it does indeed. Let’s see it in action before we talk about why I don’t like this option so much:

As seen in the example, it works well, but with an issue, it is also inverting the images. And that is because the filter property additionally applies to images, backgrounds, and borders. Now, this can be fixed easily by avoiding the change in images (or, better said, by inverting the inversion?):

html[data-theme='dark'] {
    background: #000;
    filter: invert(1) hue-rotate(180deg)
}

html[data-theme='dark'] img {
    filter: invert(1) hue-rotate(180deg)
}

Nice! but everything transitions so abruptly, let’s make it nicer with some nice transitions and some colors here and there:

html {
    transition: color 300ms, background-color 300ms;
}

html[data-theme='dark'] {
    background: #000;
    filter: invert(1) hue-rotate(180deg)
}

html[data-theme='dark'] img {
    filter: invert(1) hue-rotate(180deg)
}

Beautiful! It just works. Now, will not always look great; in some cases, it may look a bit strange, and we may want to have more control over each color.


Dark Mode with CSS Variables

This is the technique I use on my website, and it works pretty well. CSS allows the definition of custom properties (variables), which allows setting specific values that are reused throughout your website. You can find detailed information on the MDN docs.

Let’s see how they work in a simple example:


:root {
    --my-var: blue;
}

element {
    color: var(--my-var);
}

Very simple, to name a variable, we need to prepend -- to the variable name, and to use it, we use the function var().

Now let’s go back to our scenario and try to implement dark mode this way.

html {
    --background-primary: #fff;
    --color-primary: #333;
}

html[data-theme='dark'] {
    --background-primary: #030303;
    --color-primary: #ccc;
}

html {
    transition: color 300ms, background-color 300ms;
    background: var(--background-primary);
    color: var(--color-primary);
}

h1, h2, p {
    color: var(--color-primary);
}

It is much more cumbersome; now, we need to assign to each element we use on our site the colors we want for text and background. Otherwise, it won’t work. But it gives you full control over what’s happening, and you can create different variables for titles, buttons, callouts, you name it.

Let’s see this in a live example:

Now, that’s a Dark Mode I love. So far, we have explained the CSS side of things, but we will also work on the JavaScript code to make the switch and even save it on local storage so the next time the user enters the site, we remember his preference.


Light and Dark Node Switch Logic

Let’s now work on the JS that makes the switch possible. It is, again, very simple:

const htmlEl = document.getElementsByTagName('html')[0];
const toggleTheme = (theme) => {
    htmlEl.dataset.theme = theme;
}

What we did so far? The way the theme works is by applying a data attribute over the HTML element; for that, we need to capture the element in the variable htmlEl, and then finally, we have a function that receives a theme name and changes the attribute.

But we also want to make sure our changes are stored on the browser so that we remember the user’s preference for the next time he/she visits our site. For that, we will use the localStorage API.

// Capture the current theme from local storage and adjust the page to use the current theme.
const htmlEl = document.getElementsByTagName('html')[0];
const currentTheme = localStorage.getItem('theme') ? localStorage.getItem('theme') : null;
if (currentTheme) {
    htmlEl.dataset.theme = currentTheme;
}

// When the user changes the theme, we need to save the new value on local storage
const toggleTheme = (theme) => {
    htmlEl.dataset.theme = theme;
    localStorage.setItem('theme', theme);
}

Done! Let’s see it live:

So far, if there is no theme set by the user for the website, we are defaulting with the light theme, but wouldn’t it be better if we detect the system preference on color schemes and adapt our initial theme from there?

Let’s build that up:

// Capture the current theme from local storage and adjust the page to use the current theme.
const htmlEl = document.getElementsByTagName('html')[0];
const defaultTheme = window.matchMedia("(prefers-color-scheme:dark)").matches ? 'dark' : 'light';
const currentTheme = localStorage.getItem('theme') ? localStorage.getItem('theme') : defaultTheme;

if (currentTheme) {
    htmlEl.dataset.theme = currentTheme;
}

// When the user changes the theme, we need to save the new value on local storage
const toggleTheme = (theme) => {
    htmlEl.dataset.theme = theme;
    localStorage.setItem('theme', theme);
}

Here is the full demo:

The new code simply uses the matchMedia API to detect if the user’s system settings prefer a dark color scheme over a light one, and if so, it sets the right default theme.

In essence, if you need to check if the user settings prefer the dark scheme, you can use:

const prefersDarkMode = window.matchMedia("(prefers-color-scheme:dark)").matches; // true

Conclusion

As you can see, creating a dark mode is fairly simple; with a few lines of CSS and some basic logic on JavaScript, we created an easy switch that changes the colors of our website in a matter of milliseconds.

In real-world scenarios, though, you may want to use more variables to ensure you have full control over all your elements, which may require more effort and lines of code.

Still, setting up a dark mode is not hard to achieve; it’s just a matter of planning what you want to do and understanding the syntax needed to achieve it.

Thanks for reading!

Newsletter

Subscribe to my weekly newsletter for developers and builders and get a weekly email with relevant content.

    You can learn more about what to expect in these emails here.