Sassy dark mode

Backward compatible dark mode

Jump to
Dart Sass
LibSass

/i\ Now with Dart Sass version.

The easiest way to implement dark mode these days is with CSS3 custom properties (variables). Just have a :root element with default colors, and then another one under a media query, and let the user preference do the work. Something like this:

:root {
    --bkg:          white;
    --txt:          black;
}

@media ( prefers-color-scheme: dark ) {
    :root {
        --bkg:      black;
        --txt:      white;
    }
}

body {
    background:     var(--bkg);
    color:          var(--txt);
}

And if the browser doesn’t understand or have the preference, it would take the first set of variables. But if it doesn’t understand variables at all, that’s a problem.

Dart Sass

Hugo uses LibSass, which will eventually become deprecated, so I figured out how to do it in Dart Sass. The old version is still available.

In a way, it’s easier. We will use one file _theme.scss. In it, we are going to declare that we will use a map, create two maps for two color schemes, and make a mixin of our style, which we will paste twice: once as a fallback default and once for a specific color theme preference. So, we want something like this:

@use "sass:map";

$light-theme-map: (
    "background":   white,
    "color":        black,
);

$dark-theme-map: (
    "background":   black,
    "color":        white,
);

@mixin mixin-colors( $our-theme ) {
    body {
        background: map.get( $our-theme, "background" );
        color:      map.get( $our-theme, "color" );
    }
}

@include mixin-colors( $light-theme-map );

@media screen and ( prefers-color-scheme: dark ) {
    @include mixin-colors( $dark-theme-map );
}

Looking from the bottom, we see two @include’s with one variable each. One is a fallback with $light-theme-map that would be displayed in very old browsers, and the other is specific with $dark-theme-map.

They both pull in our main thing, which is a @mixin called mixin-colors() (original, I know), that we have higher up. This is our main CSS for all color styles. It is in there, where we are getting colors from maps with map.get( $map, "key" ).

Lastly (almost), we have two maps with colors for light and dark themes. They are more or less self-explanatory and resemble other variables, but they have slightly different syntax (to confuse me).

This approach has one drawback. As far as I understand, you can put a variable inside a map and manipulate it as usual. However, you can’t retrieve that variable from the map itself. In other words, you can’t use map.get() on the same map. So if I want to use one color to morph into other colors, I should first declare it as a variable and then pull derivatives into the map.

And the actuall last thing is declaring that we will use a map: @use "sass:map";. Then, we simply use our _theme.scss file in our main style.scss file by putting @use "theme"; at the top of it, and that’s it.

We could put our mixins in a different spot, and do all sorts of other fancy things, but that is outside the scope of this post. To be frank, I still don’t use Sass that much outside of this case.

LibSass

I really hope there is a better solution, because mine feels very silly, but it works. Sass variables. Bare minumum primer: Sass is a preprocessor, it creates a regular CSS file and all the magic happens before that. Also, there’s a pain point if you’re adapting an existing stylesheet: it’s better if the color information has its own section. Lucky for me, I have been doing this since time immemorial.

Ok, to get to the point, we need 6 files. Did I mention it’s silly? style.scss is our main file, which would be our actual style.css at the end. _colors.scss is our color information where we will write it with our variables. Two files for the light and dark variables themselves and two to make a magic trick work.

So, the magic trick is this: we import two files into our main file, and both of those files import a set of variables and our colors file:

_vars_light.scss + _colors.scss → _magic_light.scss

_vars_dark.scss + _colors.scss → _magic_dark.scss

_magic_light.scss + _magic_dark.scss → style.scss

So style.scss would have this:

@import "magic_light";
@media ( prefers-color-scheme: dark ) {
    @import "magic_dark";
}

_magic_light.scss would have this:

@import "vars_light";
@import "colors";

_vars_light.scss would have this:

$bkg:               white;
$txt:               black;

And _colors.scss would have this:

body {
    background:     $bkg;
    color:          $txt;
}

_magic_dark.scss and _vars_dark.scss would “mirror” _magic_light.scss and _vars_light.scss.

BTW, there may be different requirements for how Sass files are handled, I just go by what Hugo does with them and what works for me.

In the end, we have a rather large style.css file with a default light style and a dark style under a media query. Kind of like what we did with CSS3 variables, but that works without them. Silly!


A bit more about Sass.