Dark Mode in 3 Lines of CSS and Other Adventures

Dark Mode is a design choice where a website has a dark background with light-colored text and elements. It's also called Night Mode or Dark Theme. The reason for Dark Mode is to make it easier for the eyes in low light situations, save battery power on mobile devices, and create a stylish and modern look.

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>

In a stylesheet, we’ll add:

body {
  color-scheme: light dark;
}

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;
}

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%);
}

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);

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);
}

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%);
  }
}

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')
}

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 */
}

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>

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;
}

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; }

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%);
  }
}

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">

Then, in JavaScript:

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

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)
  }
})

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;

Demos

Toggler

System Colors


No comments:

Post a Comment