Skip to main content

Dark Mode

Dark mode” color schemes feature light foreground colors on dark background colors, resulting in less light emitted from the display than with “light mode” color schemes. Dark mode is great in low-light environments and uses less energy when using OLED displays. Some people find that dark mode reduces eye strain due to the low light, while others find that dark mode increases eye strain due to poor contrast. Some people adjust the color scheme according to the time of day, using light mode during the day and dark mode before going to sleep.

Personally, I prefer different color schemes in different applications. I spend most of my time in terminals, where I use dark mode. I do not use dark mode in my browser, however, as I prefer to see web content as designed.

Some operating systems allow users to configure a preference between dark mode and light mode, and this preference is exposed to web content via the CSS prefers-color-scheme media query. Using this feature, developers can automatically use a dark mode color scheme or light mode color scheme according to the user’s preference. As I write this, browser support is pretty good.

I plan on adding support for dark mode to this website in the (near?) future. I am currently working on FeedPipe, and I realized that adding support for dark mode to the built HTML manuals would be good practice. I am not ready to release FeedPipe yet, so I created a small demo for this blog post.

The objectives for the manuals and demo are as follows:

  • The implementation must be a single HTML file. All images, CSS, and JavaScript must be embedded in the HTML, not separate files.
  • The implementation must switch between dark mode and light mode according to the media query. If the user has not manually set a mode, the page must switch modes automatically when there is a change of preference. This must work even when JavaScript is disabled.
  • The implementation must use light mode by default. In other words, light mode is used if the media query is not supported.
  • When JavaScript is enabled, a control icon must be displayed to allow the user to switch modes manually. Clicking on the icon switches modes and disables automatic switching based on preference. Different icons must be displayed in dark mode and light mode, and no icons are displayed when JavaScript is disabled.
  • When JavaScript is enabled, the favicon must be updated to indicate the current mode. When JavaScript is disabled, no favicon is displayed.

The demo is available on GitHub: dark-mode-demo.html. Please note that the code for this demo is only demonstration quality and has not been polished or extensively tested.

Overview

The HTML file has the following structure:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Dark Mode Demo</title>
    <link rel="icon" id="favicon" href="javascript:void(0)" />
    <style>
      ...
    </style>
  </head>
  <body class="scheme-auto">
    <main> ... </main>
    <div id="scheme-switch"> ... </div>
    <div class="hidden"> ... </div>
    <script> ... </script>
  </body>
</html>

The favicon identifier has to be defined so that it can be referenced by JavaScript.

The style part contains the CSS definitions, and the body has four parts:

  1. The main part defines the content.
  2. The div with identifier scheme-switch part defines the control interface.
  3. The div with class hidden part contains the icons.
  4. The script part contains the JavaScript code.

The body element is assigned class scheme-auto by default. This class indicates that the user has not manually switched modes. When a user manually switches modes, this class is removed. Class scheme-dark is used to indicate that dark mode is selected. No class is used to indicates that light mode is selected, since light mode is the default.

Style

In this minimal demo, only the background-color and color of the body element need to change according to the color scheme. The default is light mode.

body {
  background-color: #fff;
  color: #121212;
}

If dark mode is selected manually, then the body element is assigned class scheme-dark.

body.scheme-dark {
  background-color: #121212;
  color: #eee;
}

The demo content is just a message in the center of the screen. The following CSS incantations center the text and select the font.

body, html {
  height: 100%;
  margin: 0;
}
main {
  align-items: center;
  display: flex;
  height: 100%;
  justify-content: center;
}
#msg {
  font-family: "Noto Sans", Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 48pt;
  font-weight: bold;
}

The control interface is displayed at the top right of the screen. Note that it has the same background color in both light mode and dark mode.

.control {
  background-color: #121212;
  border-radius: 4px;
  cursor: pointer;
  padding: 4px;
  position: absolute;
  right: 4px;
  top: 4px;
  width: 26px;
}

As is customary, the hidden class is used to hide things from the screen.

.hidden {
  display: none;
}

The following media query is used define styles when on a screen and the user prefers dark mode. The styles should all start with body.scheme-auto so that they are only used when the user has not manually selected a mode.

@media screen and (prefers-color-scheme: dark) {
  body.scheme-auto {
    background-color: #121212;
    color: #eee;
  }
}

The following media query is used to define styles when printing. Note that light mode is used when printing, but transparent background colors are used instead of white (#fff). The styles are marked as !important to make sure that they take precedence.

@media print {
  body {
    background-color: transparent !important;
    color: black !important;
  }
  .control {
    display: none !important;
  }
}

In summary, colors must be set in four places:

  • The default styles (body) use light mode colors.
  • The scheme-dark styles (body.scheme-dark) use dark mode colors. These styles are used when dark mode is selected manually, not when dark mode is selected by the media query.
  • Under the (prefers-color-scheme: dark) media query, the scheme-auto styles (body.scheme-auto) use dark mode colors. These styles are used when dark mode is selected by the media query, not when dark mode is selected manually.
  • Under the print media query, the styles (body) use light mode colors, with transparent background colors instead of white (#fff).

Content

The demo content is simply a message that is displayed in the center of the screen.

<main>
  <div id="msg">Dark Mode Demo</div>
</main>

Control Interface

The control interface contains two icons, one for light mode and one for dark mode. The control interface is hidden by default, as it is only shown when JavaScript is enabled. JavaScript shows one icon and hides the other, according to the current mode.

<div id="scheme-switch" class="control hidden">
  <span id="scheme-switch-dark"> ... </span>
  <span id="scheme-switch-light"> ... </span>
</div>

I first implemented the demo using Firefox and used emoji for the icons.

<span id="scheme-switch-dark">&#x1F319;</span>
<span id="scheme-switch-light">&#x1F4A1;</span>

Unfortunately, Chrome does not support emoji yet. (Chrome is the IE 6 of Today!) To make the demo work in Chrome, I needed to embed the icons into the HTML. I was able to get the SVG source for the Firefox icons:

The SVG is embedded in a separate part of the page and referenced by identifier in the control interface code. For example, the following is a reference to the moon icon.

<svg width="24" height="24" x="0px" y="0px" viewBox="0 0 512 512">
  <use xlink:href="#moon"></use>
</svg>

The icons are defined in the hidden div as follows:

<div class="hidden">
  <svg xmlns="http://www.w3.org/2000/svg">
    <defs>
      <g id="moon">
        ...
      </g>
      <g id="bulb">
        ...
      </g>
    </defs>
  </svg>
</div>

JavaScript

The JavaScript code is organized as follows:

(function() {
  "use strict";

  function makeDataURL(foreground, background) { ... }

  const faviconEle = document.getElementById("favicon");
  const lightDataURL = makeDataURL("#121212", "#fff");
  const darkDataURL = makeDataURL("#eee", "#121212");

  var isDarkScheme = false;
  var isManual = false;
  var query = null;

  function updateIcons() { ... }

  function setSchemeFromQuery() { ... }

  function setSchemeManual() { ... }

  function init() { ... }

  init();
})();

The makeDataURL function creates a favicon that indicates the current color scheme.

function makeDataURL(foreground, background) {
  const canvas = document.createElement("canvas");
  canvas.height = 32;
  canvas.width = 32;
  const ctx = canvas.getContext("2d");
  ctx.fillStyle = background;
  ctx.fillRect(0, 0, 32, 32);
  ctx.fillStyle = foreground;
  ctx.fillRect(10, 10, 12, 12);
  return canvas.toDataURL("image/png");
}

The favicon element is loaded, and the icons for both modes are calculated once.

The isDarkScheme variable is set to true when in dark mode. The isManual variable is set to True when the user selects a mode manually. The query variable is used for interfacing with the media query, if matchMedia is supported by the browser.

The init function is run when the page loads. If matchMedia is supported by the browser, then the media query is initialized, an event listener is added so that any changes of preference are handled, and the mode is set according the current preference. In any case, a click event handler is added to the control interface, and the control interface is displayed.

function init() {
  if (window.matchMedia) {
    query = window.matchMedia("(prefers-color-scheme: dark)");
    query.addEventListener("change", setSchemeFromQuery);
    setSchemeFromQuery();
  }
  const switchEle = document.getElementById("scheme-switch");
  switchEle.onclick = setSchemeManual;
  switchEle.classList.remove("hidden");
}

When the user changes preference, the setSchemeFromQuery function is called. If the user has not selected the mode manually, the current preference is updated and the icons are updated.

function setSchemeFromQuery() {
  if (!isManual) {
    isDarkScheme = query.matches;
    updateIcons();
  }
}

The updateIcons function updates the control interface icon and favicon according to the current mode.

function updateIcons() {
  if (isDarkScheme) {
    document.getElementById("scheme-switch-light").classList.remove("hidden");
    document.getElementById("scheme-switch-dark").classList.add("hidden");
    faviconEle.href = darkDataURL;
  } else {
    document.getElementById("scheme-switch-dark").classList.remove("hidden");
    document.getElementById("scheme-switch-light").classList.add("hidden");
    faviconEle.href = lightDataURL;
  }
}

When the user clicks on the control interface, the setSchemeManual function is called. This function sets the isManual flag, toggles the isDarkScheme flag, updates the icons, and updates the body class that indicates the current mode.

function setSchemeManual() {
  isManual = true;
  isDarkScheme = !isDarkScheme;
  updateIcons();
  if (isDarkScheme) {
    document.body.classList.add("scheme-dark");
  } else {
    document.body.classList.remove("scheme-dark");
  }
  document.body.classList.remove("scheme-auto");
}

Testing

To test, download the demo and open it in your browser. I recommend using Firefox, which has built-in developer tools that make it easy to do the following tests.

  1. Load the demo with JavaScript enabled.
    • The mode should be set according to your preference.
    • The control interface should be displayed.
    • The favicon should indicate the mode.
  2. Use your browser developer tools to toggle your preference.
    • The mode should change automatically.
    • The icon in the control interface should change according to the mode.
    • The favicon should change according to the mode.
  3. Click on the control interface.
    • With each click, the mode should change.
    • The icon in the control interface should change according to the mode.
    • The favicon should change according to the mode.
  4. Use the browser developer tools to toggle your preference.
    • The mode should not longer change because you have selected a mode manually.
  5. When in light mode, display a print preview.
    • The print preview should be in light mode.
    • The control interface should not be printed.
  6. When in dark mode, display a print preview.
    • The print preview should be in light mode.
    • The control interface should not be printed.
  7. Use the browser developer tools to disable JavaScript for the demo tab.
    • The mode should be set according to your preference.
    • The control interface should not be displayed.
    • There should be no favicon for the tab.
  8. Use your browser developer tools to toggle your preference.
    • The mode should change automatically even with JavaScript disabled.
Author

Travis Cardwell

Published

Tags