Dark Mode in 3 Lines of CSS and Other Adventures
Tags: css, webdev, tutorial, javascript
Published: 2023-04-29 12:25:08

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vokvhit3uh0vs88gbdok.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/adkskno41fct8vm5w32r.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0u8y3cflvivtt6k9tfzn.png)

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:

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

In a stylesheet, we’ll add:

```css
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ffdm4h5oygx6ktmisl5s.png)

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

We can fix that quickly, by using [System Colors](https://developer.mozilla.org/en-US/docs/Web/CSS/system-color):

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

Let’s check our snippet again:

![Dark Mode with System Colors](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p0dgpy9wtwy15dtehkwi.png)


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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aa3jwyvfoy3j2fna0xlt.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/55rk0lpr1m4aqycw8gtd.png)

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`:

```css
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iz56co00ha7z8e77cqzw.png)

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

Using [relative colors](https://www.stefanjudis.com/notes/new-in-css-relative-colors/) in CSS, we can do exactly that:

```css
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:

```css
/* 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:

```css
@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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xxy4wm3i5q3ouk2flnzb.png)


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](https://tailwindcss.com/), 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:

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

[Open Props](https://open-props.style/) is using a similar method, but updating a `data-theme`-attribute instead, and then defining properties in two blocks:

```css
[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:

```html
<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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b3frm9oscp8h7bh8wnrx.png)

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

```css
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`:

```css
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`:

```css
@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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x5fqr4oxxl5jju0v8ijs.png)

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>`:

```html
<fieldset id="colorScheme">
```

Then, in JavaScript:

```js
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:

```js
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:

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

----------

## Demos

**Toggler**

{% codepen https://codepen.io/stoumann/embed/KKGNbQr %}

**System Colors**

{% codepen https://codepen.io/stoumann/embed/GRYNPzy %}

---

Photo-credit: [Sora Shimazaki](https://www.pexels.com/da-dk/foto/mand-baerbar-computer-browsing-internet-5926379/)