Quick and Easy Dark Mode Support Without Using JavaScript

Back to blog

Quick and Easy Dark Mode Support Without Using JavaScript

11/25/2021

Dark mode has gone mainstream. I'm sure you've noticed over the last few years how more devices and operating systems are supporting dark mode out-of-the-box. If a person's OS and apps can adapt to a dark mode preference, then surely the websites they visit or the web apps they use should adapt too?

YouTube homepage in light and dark mode side-by-side

Luckily, it is possible to add dark mode support to your existing codebase quickly and easily! There's no need to leverage any sort of JavaScript or save preferences anywhere. We can leverage the dark mode support provided by modern operating systems to easily (and dynamically, if a person swaps between light and dark whilst using their device like some sort of mad colour-obsessed goblin) swap between colours using nothing but CSS.

The approach I'm about to outline is one of a few ways to achieve a dark mode. Beyond what I am about to show here, it's possible to use JS or swap out stylesheets - these are nifty tools, but outside of the scope of this guide. I think it's better to have something that works which you can iterate upon later when needed.

At its core, this approach is what we use on BBC Sounds to swap between our colour modes. It's scalable and production-ready. Best of all, it's simple.

Here is a repository with the completed steps if you want to follow along or copy + paste this into your own code

Step 1: Sort out your colour palette

The first step is to get your colour palette in order. There are important reasons for sorting your colour palette out early:

  1. You can figure out which colours to use in light mode and dark mode.
  2. You can figure out where those colours are used.
  3. Your designers, developers and testers know exactly what colours can be used. Having a shared language to use around colour (e.g., "We should use brand primary for hover states") helps simplify cross-discipline communication.

Here's a super-simple sample colour palette:

A sample colour palette, from light grey to dark purple with bright orange as an accent

Here's what that palette might look like as JSON:

{
  "Platinum": "e8e8e8",
  "Gray X 11 Gray": "bfbfbf",
  "Manatee": "96939b",
  "English Violet": "564256",
  "Mandarin": "fc814a"
}

Step 2: Use the colour palette in CSS

Now that you have your colour palette sorted, it's time to add it to your CSS. We're going to leverage three features of modern CSS: the :root psuedo-class, custom properties, and CSS variables. Together, these will expose our colour palette as variables throughout our CSS, which we can then re-use.

/* src/index.css */

:root {
  /* Colour palette */
  --platinum: #e8e8e8ff;
  --gray-x-11-gray: #bfbfbfff;
  --manatee: #96939bff;
  --english-violet: #564256ff;
  --mandarin: #fc814aff;
}

These custom properties should be added somewhere high up in your CSS architecture (e.g., src/index.css), since these values will need to be visible throughout your codebase. If you're not sure where to put these, ask your lead developer.

Step 3: Add colour implementations in CSS

With the colour palette available, the next step is to define where the colours are used. Here's a simple example:

/* src/index.css */

:root {
  /* Colour palette */
  --platinum: #e8e8e8ff;
  --gray-x-11-gray: #bfbfbfff;
  --manatee: #96939bff;
  --english-violet: #564256ff;
  --mandarin: #fc814aff;

  /* Colour implementations */
  --page-background: var(--platinum);
  --text-primary: var(--english-violet);
  --link-text: var(--mandarin);
}

This is important because it decouples the definition and implementation of the colour. This might seem obtuse now, but please stick with it because I promise it will make sense shortly!

We can use our colour implementations like this:

/* src/page.css */

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

p {
  color: var(--text-primary);
}

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

Step 4: Add colour implementations for dark mode

This is the magic step. Using media queries, it's possible to determine if the visiting device has dark mode enabled by using the prefers-color-scheme: dark query. Let's add this in to our colour definitions:

/* src/index.css */

:root {
  /* Colour palette */
  --platinum: #e8e8e8ff;
  --gray-x-11-gray: #bfbfbfff;
  --manatee: #96939bff;
  --english-violet: #564256ff;
  --mandarin: #fc814aff;

  /* Light mode colour implementations */
  --page-background: var(--platinum);
  --text-primary: var(--english-violet);
  --link-text: var(--mandarin);
}

/* Dark mode colour implementations */
@media (prefers-color-scheme: dark) {
  :root {
    --page-background: var(--english-violet);
    --text-primary: var(--platinum);
    --link-text: var(--mandarin);
  }
}

We can simply assign new values to our colour implementations within the prefers-color-scheme: dark media query. This is why we've separated the definitions and implementations. I hope it makes sense!

Boom. That's it. Now if you swap between light and dark mode, you'll see that the colours dynamically shift between the two. If you need to add more colours to your palette or add more colour implementations, you can do so in the same stylesheet we have been working on.

To Conclude

So, in four simple steps we can get comprehensive support for light and dark mode on our websites and apps. This approach is performant and standards-compliant, so there's no need to worry about edge cases or incompatibilities in modern browsers.

Bonus: Progressive enhancement

Sometimes you may need to support older operating systems or older browsers, and it may appear that this approach falls apart at that point. Luckily, there's a very easy way to ensure older browsers are supported using progressive enhancement.

Browsers ignore CSS they don't understand, so we can add support for older browsers by including fallback rules they do understand.

/* src/page.css */

html {
  background-color: #e8e8e8ff; /* old browsers understand this, so they use this rule */
  background-color: var(--page-background); /* old browsers don't understand this, so they ignore this rule */
}

p {
  color: #564256ff;
  color: var(--text-primary);
}

a {
  color: #fc814aff;
  color: var(--link-text);
}

IMPORTANT: Make sure you put the fallback rules before the rule with the var(). If you don't, your fallback rules will overwrite your fancy new light/dark mode switching behaviour!