Dark Mode in 3 Lines of CSS and Other Adventures

madsstoumann

Mads Stoumann

Posted on April 29, 2023

Dark Mode in 3 Lines of CSS and Other Adventures

Dark Mode is a design trend where the color scheme of a website is changed to a dark background with light-colored text and elements. It's also referred to as Night Mode or Dark Theme. The purpose of Dark Mode is to reduce eye strain in low light environments, conserve battery life on mobile devices, and create a sleek and modern aesthetic.

Many popular websites and applications now offer a Dark Mode option — here’s TailwindCSS:

Tailwind Color Scheme Selector

If you’re a developer, you most likely already know how to toggle Dark Mode in Dev Tools (for those who don’t know, go to the “rendering” tab!):

Dev Tools Color Scheme Selector

If you want to toggle on Dark Mode for your operating system (and thus all apps supporting Dark Mode), go to System Settings. On a Mac, you‘ll find it under System Settings > Appearance:

MacOS Toggle Dark Mode

There are a lot of tutorials out there already on how to implement dark mode for a webiste.

This tutorial will look into different approaches, including some rad new CSS-features, that’ll allow you to ditch JavaScript.

Ready? Let’s get started.


Dark Mode using System Colors

First, we’ll create a simple HTML-document with a headline:



<body>
  <h1>Hello Darkness, My Old Friend</h1>
</body>


Enter fullscreen mode Exit fullscreen mode

In a stylesheet, we’ll add:



body {
  color-scheme: light dark;
}


Enter fullscreen mode Exit fullscreen mode

This will tell the browser, that our document can accept both a light and a dark color-scheme.

If you run this snippett in a browser, even with Dark Mode on, it will be … light.

Your browser UI will be dark, though.

Light Dark

That’s because the user-agent-stylesheet does not have any default colors set up.

We can fix that quickly, by using System Colors:



body {
  background-color: Canvas;
  color: CanvasText;
  color-scheme: light dark;
}


Enter fullscreen mode Exit fullscreen mode

Let’s check our snippet again:

Dark Mode with System Colors

Much better! Dark Mode for your entire website in 3 lines of CSS!

Let’s dive a bit more into System Colors. From the specs:

In general, the <system-color> keywords reflect default color choices made by the user, the browser, or the OS. They are typically used in the browser default stylesheet, for this reason.

Here’s a Light Mode-demo, showing the available System Colors in Safari:

System Colors

If we switch to Dark Mode, some colors are changed completely (like Canvas and CanvasText as we’ve already encountered), while others are only slightly changed:

System Colors in Dark Mode

Using System Colors for Dark Mode is a bit of a simplified Dark Mode-experience.

Yes, it will work — but pure black and white is a bit boring.

We can spice it up using color-mix in CSS. We can mix a bit of CanvasText (black or white) into Canvas (white or black) for the background-color, and the opposite for color:



body {
  background-color: color-mix(in srgb, Canvas, CanvasText 2.5%);
  color: color-mix(in srgb, CanvasText, Canvas 15%);
}


Enter fullscreen mode Exit fullscreen mode

This results in a “softer” look:

Softer Dark Mode

Deducting saturation from a color, is a widely used way to make variations of colors in Dark Mode.

Using relative colors in CSS, we can do exactly that:



background: hsl(from ActiveText h calc(s - 30%) l);


Enter fullscreen mode Exit fullscreen mode

Unfortunately, relative colors don’t work with System Colors in any browsers — yet!

NOTE: System Colors can be overwritten with forced colors (although used rarely) — so don’t rely too much on this technique.

Let’s move on to another technique, that’ll allow us to fine-control our Dark Mode colors.


Using a prefers-color-scheme media-query

To specify specific colors for light and dark mode, I recommend using CSS Custom Properties, and then update these using a prefers-color-scheme media-query.

With Light Mode as our default, let’s add the colors to a :where(body)-section, to separate them from our regular body-styles:



/* Properties */
:where(body) {
  --background-color: #FFF;
  --text-color: #222;
}
body {
  background-color: var(--background-color);
  color: var(--text-color);
}


Enter fullscreen mode Exit fullscreen mode

Then, for Dark Mode, we’ll simply update these colors-properties:



@media (prefers-color-scheme: dark) {
  :where(body) {
    --background-color: hsl(228, 5%, 15%);
    --text-color: hsl(228, 5%, 80%);
  }
}


Enter fullscreen mode Exit fullscreen mode

Let’s check it out:

Dark Mode with media-query

But what if we want the users to choose which version of our website they want, indepently of the System Settings?

They might prefer a system set to Dark Mode, but our website in Light Mode.

Let’s create a toggler!


Creating a color-scheme toggler

Using JavaScript

If you go to a site like TailwindCSS, you’ll notice that a dark-class is added to the html-node when you select ”dark” from the color-scheme-toggler. This is done with JavaScript:



window.matchMedia('(prefers-color-scheme: dark)').matches)) {
  document.documentElement.classList.add('dark')
} else {
  document.documentElement.classList.remove('dark')
}


Enter fullscreen mode Exit fullscreen mode

Open Props is using a similar method, but updating a data-theme-attribute instead, and then defining properties in two blocks:



[data-theme=light] {
  --nav-icon: var(--gray-7); /* etc */
}
[data-theme=dark] {
  --nav-icon: var(--gray-5); /* etc */
}


Enter fullscreen mode Exit fullscreen mode

Using CSS

Using some brand new CSS-techniques, we can create a toggler without JavaScript.

We’ll create a toggler with 3 states:

  • Light (forced)
  • Auto (system default, could be either light or dark)
  • Dark (forced)

First, some basic markup:



<fieldset>
  <label>
    <input type="radio" name="color-scheme" id="color-scheme-light" value="0">
    Light
  </label>
  <label>
    <input type="radio" name="color-scheme" value="auto" checked>
    Auto
  </label>
  <label>
    <input type="radio" name="color-scheme" id="color-scheme-dark" value="1">
    Dark
  </label>
</fieldset>


Enter fullscreen mode Exit fullscreen mode

After adding some basic styles (see the Codepen-demo below), it renders like this:

Basic Dark Mode Toggler

We’ll add a --darkmode-property and container-type to the html-element:



html {
  --darkmode: 0;
  container-name: root;
  container-type: normal;
}


Enter fullscreen mode Exit fullscreen mode

We’ll be using @container style()-queries, so we need to set the node as a “container”.

Since we don’t want to observe inline-size-changes, we just add the value normal.

If the user picks a “forced” value, we’ll update --darkmode:



html:has(#color-scheme-light:checked) { --darkmode: 0; }
html:has(#color-scheme-dark:checked) { --darkmode: 1; }


Enter fullscreen mode Exit fullscreen mode

And finally, we’ll use a container style()-query to check, if --darkmode is set to 1:



@container root style(--darkmode: 1) {
  body {
    --background-color: hsl(228, 5%, 15%);
    --text-color: hsl(228, 5%, 80%);
  }
}


Enter fullscreen mode Exit fullscreen mode

NOTE: @container style()-queries only works behind-a-flag in Chrome at the moment, it’s early days, so do not use in production.

Now, after selecting “Dark”, our toggler (and page) looks like this:

Toggler in Dark Mode

Simple and effective!


Storing state

If we want to store the users choice, I’m afraid we need a bit of JavaScript!

First, let’s add an identifier to our <fieldset>:



<fieldset id="colorScheme">


Enter fullscreen mode Exit fullscreen mode

Then, in JavaScript:



const colorScheme = document.getElementById('colorScheme')
colorScheme.addEventListener('change', event => {
  localStorage.setItem('color-scheme', event.target.value)
})


Enter fullscreen mode Exit fullscreen mode

Now, all we have to do is setting the property to the localStorage-value when the document has been loaded:



window.addEventListener("load", event => {
  const scheme = localStorage.getItem('color-scheme') || 'auto'
  if (scheme) {
    document.documentElement.style.setProperty('--darkmode', scheme)
  }
})


Enter fullscreen mode Exit fullscreen mode

To select the correct mode in the toggler, add this to the if-block:



const selected = [...colorScheme.elements].filter(element => element.value === scheme)
if (selected) selected[0].checked = true;


Enter fullscreen mode Exit fullscreen mode

Demos

Toggler

System Colors


Photo-credit: Sora Shimazaki

💖 💪 🙅 🚩
madsstoumann
Mads Stoumann

Posted on April 29, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related