Our Dark Mode Setup

by — posted on

Conceptually, dark/light mode on a website is driven by the buttons on the UI: whichever mode user picks, a data-theme attribute with that value gets set on <html> tag. We also persist that to browser's local storage.

Automatic mode setting "listens" to system's setting via @media (prefers-color-scheme: dark). Manual dark/light modes' CSS "listens" to <html> tag attribute's value by :root[data-theme='dark'].

That's pretty much it.

Here is the mixin we use.

In these days, PostCSS plugins can substitute SASS. Dropping SASS simplifies the tooling: there's one part less in the setup (pun intended). We use postcss-comment, postcss-import, postcss-mixins, postcss-simple-vars to process all mixins and variables.

PostCSS mixin:

@mixin darkTheme {
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) & {
:root[data-theme='dark'] & {

Use it like that:

/* color constants (could be native CSS variables too) */
$emphasisGrey: #949494;
$textDark: #807e79;


color: $emphasisGrey;
@include darkTheme {
color: $textDark;

All three modes: auto, light and dark, are tackled with this simple mixin' — only one nested clause @include darkTheme {...}` is all it takes to define an alternative, dark styling.

A useful reference is this article opens in a new tab — although here we don't use native CSS variables, the gist is the same and instructions explain the theme-switching pretty thoroughly.

For and against CSS custom variables

The CSS custom properties/variables have their use; they can be an exquisite solution when the design is simple, and sets of colours can be swapped.

Here are the arguments against native CSS variables (--main-bg-color), in favor of SASS-style variables ($main-bg-color):

  • We wrote a parser to ingest CSS/SASS variables into Nunjucks global scope, to allow us to use CSS vars inline, in HTML. Switching to CSS custom properties --main-bg-color: brown; will require another program/parser...
  • SASS-style dollar notation is six characters less to type, $zzz instead of var(--zzz).
  • Not very relevant, but CSS variables are not supported 100% — IE will need extra measures, for example. On the other hand, PostCSS variables will be rendered into classic CSS and supported 100%.
  • In Web development land, it's a decision to make but in Email development land, one can't use opens in a new tab CSS variables at all while PostCSS/SASS is readily at hand and available.

Back in the day, data was often put onto <body> and using class attributes rather than more semantic data-* attribute. That's a bad habit, don't fear to put data- attributes onto <html> tags.

Related packages:

📦 lect 0.18.1
Maintenance CLI for internal consumption
📦 string-extract-sass-vars 3.0.1
Parse SASS variables file into a plain object of CSS key-value pairs