Dark Mode - The prefers-color-scheme Website Tutorial

Dark Mode - The prefers-color-scheme Website Tutorial

Dark mode has become one of the most often requested app and website design features. So, we've put together this implementation tutorial with examples. Explore prefers-color-scheme media query, how to make a mode switch with JavaScript, and what else to consider.

There are many potential benefits of dark mode, like saving battery life and reducing eye-strain in low light environments. For some users, dark mode is an accessibility feature helping with light-triggered migraine problems. Others prefer it simply for aesthetic reasons.

On most devices/operating systems, users can enable dark mode at a system level. This usually works system-wide, including default and third-party apps. But what about websites? Would it be possible to adapt to the user's preferred color scheme automatically?

'Do. Or do not. There is no try.' - Yoda

Changing the color mode settings of the operating system switches the Death Star colors:

Example 1

User system mode detected

TIP: You can test emulating prefers-color-scheme with Chrome DevTools (Customize and control DevTools > More tools > Rendering > Emulate CSS media feature prefers-color-scheme).

The CSS prefers-color-scheme media query accomplishes an instant change in appearance. Now, let's see what exactly is prefers-color-scheme and how it behaves in action - we're also going to show in this tutorial how to detect and toggle between light and dark mode using JavaScript on your website.

What Is the prefers-color-scheme Media Query Anyway?

The prefers-colors-scheme media query gets applied depending on the user's color scheme preference in the OS. For example, if you selected dark mode in your OS settings, all websites with dark mode support will be displayed in your preferred mode, applying the CSS rules defined inside the prefers-color-scheme media query. It's a new, Level 5 Media Query and already has good browser support. Learn how to enable dark mode in your OS in this article.

There are two prefers-color-scheme values to choose from:

  • light - the user has expressed preference for a page that has a light mode or has not expressed an active preference
  • dark - the user has expressed preference for a page that has a dark mode

Let's say we want to enable our website to switch to dark mode for users with such preference:

/* default, light mode styling */
.element {
  background-color: #fff;
  color: #000;
}

/* if user switches the system settings to dark mode */
/* this media query will be applied */
@media (prefers-color-scheme: dark) {
  .element {
    background-color: #000;
    color: #fff;
  }
}

Voila, we now have dark mode support.

How to Add User Choice Mode Switch With prefers-color-scheme and JavaScript

Users who have set their color preferences to the OS's dark mode may still want to see certain websites in light mode and the other way around. Let's try to toggle between the two with the help of prefers-color-scheme in our Lightsabers tutorial example:

'Not the brightest lightsaber in the galaxy' - Unknown

Example 2 - Toggle

User system mode detected

Setting Markup and Styles

The simplified markup:

<body>
  <div class="wrapper">
    <svg class="lightsaber first">...</svg>
    <svg class="lightsaber second">...</svg>
    <button class="btn">Toggle mode</button>
  </div>
</body>

Colors are the only thing that really matters to us in this example. You can see an excerpt of the styles applied below, this time using CSS variables:

/* default, light mode styles set with variables */
:root {
  --color-background: #f9f9f9;
  --color-default: #000;
  --color-accent-1: deepskyblue;
  --color-accent-2: violet;
}

body {
  background-color: var(--color-background);
}

/* Toggle button */
.btn {
  background-color: var(--color-background);
  border: var(--color-default) solid 3px;
  color: var(--color-default);
}

/* SVG lightsabers */
.first-blade {
  fill: var(--color-accent-1);
}

.second-blade {
  fill: var(--color-accent-2);
}

.handle {
  fill: var(--color-default);
}

Then we define .dark-mode class with dark mode styles. It will be toggled on button click or added automatically to apply alternative dark mode color values on the entire document.

/*dark-mode*/
.dark-mode .example-2 {
  --color-background: #000;
  --color-default: #f9f9f9;
  --color-accent-1: cyan;
  --color-accent-2: magenta;
}

Apply Dark Mode With prefers-color-scheme Using JavaScript

Now you would probably expect us to add @media (prefers-color-scheme: dark) in our stylesheet. But, we will take a different approach. One of the reasons for it is keeping our CSS as DRY as possible without adding another set of variables with the same color values. We are going to demonstrate how to use the prefers-color-scheme media query with JavaScript instead.

First, we select the toggle button.

const button = document.querySelector(".btn");

Then we create a MediaQueryList object. The useDark object will help us test the media query against the window and notify the listener when the media query state changes.

const useDark = window.matchMedia("(prefers-color-scheme: dark)");

The toggleDarkMode function takes Boolean as an argument. Depending on the argument value, it toggles the .dark-mode class on the document.documentElement (<html> element). This way, dark mode style is applied sitewide.

function toggleDarkMode(state) {
  document.documentElement.classList.toggle("dark-mode", state);
}

The useDark object has a matches property that checks the prefers-color-scheme state (dark mode being on or off in the system). useDark.matches returns a Boolean, which we will use as toggleDarkMode function input parameter for the initial mode setting since we have no prefers-color-scheme media query defined in CSS to handle that part.

If the user has dark mode enabled, useDark.matches will return true, and toggleDarkMode will add the .dark-mode class so that dark mode will be applied when you first open the website.

toggleDarkMode(useDark.matches);

Next, we’re going to add listeners to listen for changes in the OS settings and users toggle choice.

MediaQueryList object has an addListener method available. Following that, we have set an event listener on the useDark object and toggleDarkMode will be used inside the listener function to change the mode whenever the OS settings are changed. For example, by removing the .dark-mode class if the user switches from dark to light mode at the operating system level.

The evt is the event object that also has a matches property, so we can directly access it and pass the returned Boolean value into the toggleDarkMode function.

// Listening for the changes in the OS settings and auto switching the mode
useDark.addListener((evt) => toggleDarkMode(evt.matches));

Similarly, an event listener is added to the toggle button. It’s going to toggle the mode independently from the OS settings and override them.

// Toggles the "dark-mode" class on click
button.addEventListener("click", () => {
  document.documentElement.classList.toggle("dark-mode");
});

And voila again. The Lightsabers example respects your saved system color preferences but still lets you change the mode.

Addition: How to Make Persistent User Choice Mode with JavaScript using localStorage

We may want to provide users with the possibility to save the mode selection making their choice persistent. Regardless of the colors set in their system, the website interface can be updated on load with the mode preference stored in the localStorage.

The main difference between the code below and the code from the previous example is that the initial setting loads the mode based on the localStorage state, instead of based on the prefers-color-scheme value. Button click listener function toggles dark-mode in localStorage by calling the setDarkModeLocalStorage function.

let darkModeState = false;

const button = document.querySelector(".btn");

// MediaQueryList object
const useDark = window.matchMedia("(prefers-color-scheme: dark)");

// Toggles the "dark-mode" class
function toggleDarkMode(state) {
  document.documentElement.classList.toggle("dark-mode", state);
  darkModeState = state;
}

// Sets localStorage state
function setDarkModeLocalStorage(state) {
  localStorage.setItem("dark-mode", state);
}

// Initial setting
toggleDarkMode(localStorage.getItem("dark-mode") == "true");

// Listen for changes in the OS settings.
// Note: the arrow function shorthand works only in modern browsers, 
// for older browsers define the function using the function keyword.
useDark.addListener((evt) => toggleDarkMode(evt.matches));

// Toggles the "dark-mode" class on click and sets localStorage state
button.addEventListener("click", () => {
  darkModeState = !darkModeState;

  toggleDarkMode(darkModeState);
  setDarkModeLocalStorage(darkModeState);
});

Now, it not only allows us to toggle the mode, it lets us save the mode. If we select dark mode with the toggle button and reload the page or close and reopen the browser window, our dark mode will persist (even with light mode activated in the operating system).

Previously, cookies were the only way to store the data between usage sessions. Unlike localStorage, which allows us to create and keep data on the client-side, cookies are sent on the server with every HTTP request and are handled server-side. That’s why localStorage is an obvious option to store user preferences if you run a static website.

TIP: When using dynamic websites with localStorage, you should keep in mind that a flash of the default color scheme is possible while the mode is being determined. For color scheme activation without flashing, you could fetch the mode preference from localStorage before the page renders, in the <head> element of the HTML document.

<head>
  <script>
    const colorScheme = localStorage.getItem("color-scheme") || "light-mode";
  </script>
</head>

Handling Images in Dark Mode

Some images are very bright and will pop out even more from the dark background. It defeats the purpose of dark mode, especially if the images are large.

Art direction is, among other methods, selective loading with an <picture> element whose media attribute allows us to load images depending on the media query state. This way, we can load a different image when in dark mode.

<picture>
  <source srcset="light-image.jpg" media="(prefers-color-scheme: light)" />
  <source srcset="dark-image.jpg" media="(prefers-color-scheme: dark)" />
  <img src="light-image.jpg" />
</picture>

This example code will load the light image if your preferences are set to light mode, or the dark image if your preferences are set to dark mode.

Another way to make images more supportive of dark mode is to add a filter:

@media (prefers-color-scheme: dark) {
  /* we are excluding SVG */
  img:not([src$=".svg"]) {
    filter: brightness(70%);
  }
}

The color-scheme

Including the color-scheme meta tag in the document tells the browser that the document supports light, dark, or both modes. This is necessary because some system UI elements, like form controls and scrollbars, don’t adjust well to dark mode.

The following meta tag should be added to your document's head, with the value of the content attribute set to themes supported in the document:

<meta name="color-scheme" content="light dark" />

Alternatively, color-scheme can be added as a CSS property on any element in the stylesheet.

color-scheme: light dark;

Read more about color-scheme on web.dev.

Bonus Dark Mode Example

In the example below, emulating a simple website landing page, dark mode is enabled with prefers-color-scheme media query in JavaScript and can be overridden with a mode switch.

Example 3

User system mode detected

'Chewie, we’re home.' - Han Solo

We hope we've provided some insights about prefers-color-mode media query, dark mode and toggle implementation technique, as well as other things to consider when adding dark mode to your website. Here’s a further reading list on the topic of prefers-color-scheme and dark mode:

Author: