Skip to content

Dark Mode

I’ve had a dark mode for this site for a little while now, but it’s been controlled by the OS color scheme, rather than being something that can be changed on the site. An article I read on how to do it properly gave me the nudge to do the same here.

Required Reading

Step one: go read Josh Comeau’s “The Quest for the Perfect Dark Mode”, or at least skim it over before you dig in here. It’s an incredibly detailed and well explained dive into how he implemented dark mode on his site, and I stole borrowed about 90% of it.

I’m not going to go into all the details of how I’ve implemented everything, just the bits where I decided to change the implementation, so it’s not going to make much sense without that context. It’d be like reading Star Wars fanfiction before you watch Star Wars.

Setting Classes

Rather than setting the individual colours through JS, I decided to reflect the current theme with a class on the <html> element. This means our JS only needs to change 1 thing in order to toggle the theme, rather than setting each colour value, and also means components can define their own colours rather than them all being stored in one place.

setDark = dark => {
  const classes = document.documentElement.classList

  if (dark) {
    classes.add("dark")
    classes.remove("light")
  } else {
    classes.add("light")
    classes.remove("dark")
  }
}

I’m curious what the trade offs are here in terms of performance, but I want to actually publish this article tonight, not wake up at 4am with a keyboard imprint on my face and 86 tabs about JS performance profiling open (realistically it’ll probably be both).

Using Classes

The downside is repetition in the main styles, as we need to define the colours 4 times:

  1. The initial defaults.
  2. The dark defaults, in case our JS doesn’t fire.
  3. The light styles for html.light.
  4. The dark styles for html.dark.
:root {
  --color-background: #fefefe;
  --color-background-alt: #ededed;
  --color-foreground: #212121;
  --color-accent: #a40802;

  @media (prefers-color-scheme: dark) {
    --color-background: #212121;
    --color-background-alt: #323232;
    --color-foreground: #fefefe;
    --color-accent: #fbb829;
  }

  &.light {
    --color-background: #fefefe;
    --color-background-alt: #ededed;
    --color-foreground: #212121;
    --color-accent: #a40802;
  }

  &.dark {
    --color-background: #212121;
    --color-background-alt: #323232;
    --color-foreground: #fefefe;
    --color-accent: #fbb829;
  }
}

Sometimes, just use CSS

We could probably skip #2 and just default everyone to light mode, and that would mean that #3 wouldn’t be needed either. By relying on the native CSS support for the OS dark mode, our initial inline JS that reads the colour scheme only has to worry about stored preferences, the browser natively handles my less picky readers.

(const storedDark = localStorage.getItem("dark")
  if (storedDark !== null) {
    document.documentElement.classList.add(
      JSON.parse(storedDark) ? "dark" : "light"
    )
  })()

As an added bonus, users without JS will still get their preferred theme.

Fun experiments

Now that I’ve got a system wide colour scheme I can do some fun things with it. This site is more of a sandbox to play around in than anything, and I’ve had the message

This is where my under-construction.gif would go.

on my home page for a while now, so I figured “Why not have an actual construction sign that respects the colour theme”.

<svg xmlns="http://www.w3.org/2000/svg" aria-label="Under Construction" strokeLinejoin="round"  width="70px" viewBox="6 9 186 162">
  <path fill="none" stroke="var(--color-accent)" strokeWidth="17" d="M99 18L15 162h168z"/>
  <path fill="var(--color-text)" d="M101 121a2 2 0 01-3 1l-2-3v-16l-6 10-11-6 9-18h-6l-8 14-3-3v-3l8-14h20a4 4 0 014 4zm16 12L76 99l1-3 42 35 8-10a5 5 0 019 0l22 29h-57l13-16zm-28-18l7 10 1 25a8 8 0 01-6-4l-1-17-8-11-18 31a4 4 0 01-2-2l-1-6 17-32zm13-36a7 7 0 0114 0 7 7 0 01-14 0"/>
</svg>

Most of that isn’t really important, it’s just the SVG markup for a construction sign, but look! It’s using our CSS variables inline to set the colours. The end result is a fun little throwback to the 90s internet with some modern CSS tricks.