HomeBlog

Adding a Dark Mode Color Scheme With the Help of CSS Variables

10 July, 2020 - 5 min read

At the company where I'm employed I've developed a pattern library/design system which we're using to modernize the product admin. It's designed by me, and is built to be maintainable, modular, extensible and future friendly. I wanted to test the flexibility of the pattern library, to see if I had met my goals. So on spare occations I've been tinkering on a "dark mode". Just for the fun of it. And I was pleasantly surprised to find that it was surprisingly easy to implement!

The end results makes use of a CSS media feature called prefers-color-scheme and CSS custom properties. These two tools let me design two seperate color schemes for the same theme. There is no further coding required to make the theme switching work, and there is only one single style sheet. In this blog post I'll go through the very basics that make this technically work, and how to live up to the four core values - maintainable, modular, extensible and future friendly.

The (Too) Simple Method

One thing we've done in our pattern library it to define a color palette. This means that every shade of every color has a name and a variable. We only use colors from that color palette.

$color-neutral-white: hsl(0, 0, 100%);
$color-neutral-100: hsl(0, 0, 96%);
$color-neutral-200: hsl(0, 0, 88%);
// etc...

If you don't give it much thought you might think that simply invert the lightness of each shade.

// color.scss
$color-neutral-white: hsl(0, 0, 0%); // 100% - 100%
$color-neutral-100: hsl(0, 0, 4%); // 100% - 96%
$color-neutral-200: hsl(0, 0, 12%); // 100% - 88%
// etc...

Light shades become dark, and vice versa. Sure, you would get a dark theme... but if you want a good looking theme you will want more control than that. En essence there are two reasons why you can't just invert the lightness of all shades:

Just inverting lightness doesn't look good

In "light mode" most items have a 100% white background. But in "dark mode" I want to use a dark gray, rather than pitch black. I could solve this by simply mapping 100% lightness in light mode to, say, 10% lightness in "dark mode" instead of 0%. That's still not a great solution though. We would lose 10% of potential contrast, and the shades are still not custom picked to look perfect.

Not everything should be inverted

While most "dark mode" patterns use light foreground colors in dark background colors, I want some patterns to have dark text on light background, even in "dark mode". For example, in "dark mode" I still use dark text on light background for primary buttons. This makes primary buttons really stand out in a nice way. But I want to pick the shades that fit well with the dark theme, so I still want to customize the shades.

The Better Method - Two Levels of Variables

Instead of mapping color shades directly to patterns, I created a second layer of variables. For example, instead of mapping the color variable for white to the page background, I create a variable for the page background and mapped the variable for white to the page background variable. This made it very easy to create new color mappings for all the patterns. All I had to do was replace the file with the color mappings, and I could create two different themes.

// colors.scss
$color-neutral-white: hsl(0, 0, 100%);
// etc...
$color-neutral-900: hsl(0, 0, 10%);
$color-neutral-black: hsl(0, 0, 0%);
// patterns.scss
.page {
  color: $page-color;
  background-color: $page-background;
}
// light-theme.scss
@import 'color.scss';

$page-color: $color-neutral-black;
$page-background-color: $color-neutral-white;

@import 'patterns.scss';
// dark-theme.scss
@import 'color.scss';

$page-color: $color-neutral-white;
$page-background-color: $color-neutral-900;

@import 'patterns.scss';

In the trivial example above we can generate light-theme.css and dark-theme.css while re-using almost all of the code.

However, we've now ended up with two generated CSS files that need to be swapped out manually (or with code). We can do better than that...

The "Real" Solution

There is a wonderful CSS media feature called prefers-color-scheme, which tells us if the user prefers a dark or a light theme.

@media (prefers-color-scheme: dark) {
  /* Styling for dark mode */
}

This is where CSS custom properties come in handy. If we let our intermediate layer of variables become CSS variables, we can put everything in the same theme (and the same file, if we like):

// theme.scss
$color-neutral-white: hsl(0, 0, 100%);
// etc...
$color-neutral-900: hsl(0, 0, 10%);
$color-neutral-black: hsl(0, 0, 0%);

:root {
  --page-color: $color-neutral-black;
  --page-background: $color-neutral-white;

  @media (prefers-color-scheme: dark) {
    --page-color: $color-neutral-white;
    --page-background: $color-neutral-black;
  }
}

.page {
  color: var(--page-color);
  background-color: var(--page-background);
}

Voila!

  • Color shift automatically according to system preferences
  • We can mix-and-match color shades in differnt ways for light and dark themes
  • We can easily customize patterns individually

© 2020, Josef Engelfrost