The simplest CSS variable dark mode theme

I have been working on a custom Ghost theme to power my blog (you are looking at an early version right now!). One thing I wanted to have a crack at was a dark/light theme switcher. It turns out with modern CSS this is pretty straight forward.

The approaches I considered were:

  • CSS classes set on the <body>
  • Switch out style sheet entirely
  • CSS variables

I went with CSS variables because my blog audience tends to be on the latest browser versions so I don't need to worry much about browser support (not that it is too bad).

If a blog post is too much for you, I managed to condense it into 4 tweets:

Using CSS variables for themes

CSS variables are properties you can set in your stylesheets that can be used in later styles. They are similar to tools such as SASS but built directly into CSS. This makes them very well suited for theming.

This is a simplified snippet from the default variables used in my theme GhostSolo.

:root {
    --background-color: #fff;
    --text-color: #121416d8;
    --link-color: #543fd7;
}

html[data-theme='light'] {
    --background-color: #fff;
    --text-color: #121416d8;
    --link-color: #543fd7;
}

html[data-theme='dark'] {
    --background-color: #212a2e;
    --text-color: #F7F8F8;
    --link-color: #828fff;
}

The :root selector is the default set of values. When <html data-theme='dark'> is set those values are overridden by the html[data-theme='dark'] values. It is really just a matter of applying the variables in your CSS to get the effect.

body {
    background: var(--background-color);
    color: var(--text-color);
}

a {
    color: var(--link-color);
}

a:hover {
    text-decoration: underline;
    filter: brightness(80%);
}

In my theme I have variables for a couple of key style choices to allow distinct colour themes beyond dark/light.

:root {
    --background-color: #fff;
    --alternate-background-color: #f7f7f9;
    --text-color: #121416d8;
    --text-color-light: #777676bb;
    --link-color: #543fd7;
    --masthead-color: #543fd7;
    --masthead-text: #fff;
    --button-color: #263238;
    --button-text: #fff;
    --bs-font-sans-serif: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
    --bs-font-serif: Georgia, serif;
    --bs-font-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
}
Note: the --bs prefixed variables are CSS variables bootstrap 5 supports. The css variable support in Bootstrap is a bit limited and doesn't apply to things like .btn-primary or .bg-light.

JavaScript dark mode toggle

With the CSS theming in place I needed a simple way to switch between "dark" and "light" modes.

var toggle = document.getElementById("theme-toggle");

var storedTheme = localStorage.getItem('theme') || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
if (storedTheme)
    document.documentElement.setAttribute('data-theme', storedTheme)


toggle.onclick = function() {
    var currentTheme = document.documentElement.getAttribute("data-theme");
    var targetTheme = "light";

    if (currentTheme === "light") {
        targetTheme = "dark";
    }

    document.documentElement.setAttribute('data-theme', targetTheme)
    localStorage.setItem('theme', targetTheme);
};

This simple JavaScript snippet will set the theme based off the user's system preferred setting using a media query for "prefers-color-scheme: dark". It will also store the value in local storage so it can be persisted across each page. The selected theme is set as a data-theme attribute on the html node.

Other CSS theming tricks

I couldn't quite get form inputs looking nice with a pure CSS variable approach. When using data attributes, you can always target them in your CSS the traditional way. This CSS gives the inputs a nice dark overlay against any background colour but didn't look right on the light theme.

[data-theme='dark'] .form-control {
    background-color: rgba(0, 0, 0, 0.6);
    border-color: rgba(0, 0, 0, 0.6);
    color: var(--text-color) !important;
}

[data-theme='dark'] .form-control:focus {
    color: var(--text-color) !important;
}

I needed to show/hide my sun moon icons in the dark mode toggle button. I went with a Bootstrap inspired display utility approach to change an element display based on the theme.

[data-theme='light'] .d-block-light,
[data-theme='dark'] .d-block-dark {
    display: block !important;
}
<button id="theme-toggle" class="btn btn-link btn-sm ml-2 small" type="button">
 <span class="d-block-light d-none">{{> "icons/moon"}}</span>
 <span class="d-block-dark d-none">{{> "icons/sun"}}</span>
</button>

See it in action

Use the sun/moon icon in the nav bar of this page to see it in action. Or just watch the gif:

Dark mode toggle

References