Ler blog em Português

Supporting dark mode in web content

Read in 7 minutes

Welcome to 2019, the year that the technology went dark mode. Both Android 10 and iOS 13 were released last month, and the most acclaimed feature was dark mode. Native apps can now implement specific themes for each mode (i.e. dark, light), but can you do it on the web? This article outlines how you can take advantage of this new trend and make everyone happy.

Are there any benefits on using dark mode?

When you read about dark mode, you often see “easy on the eyes” or “energy efficiency” being mentioned. Truth is, to be energy efficient you need OLED/AMOLED screens which are only available on newer high-end devices.

If you’re using LCD or another type of screen, changing colors won’t do much for your battery life. Most people won’t benefit from the energy efficiency aspect of dark modes at all.

But what about health? It certainly is better, right? Well… unfortunately, we still don’t have enough scientific data to assert that dark mode is better. What we know for sure is that the time we spend looking at screens is the main factor to eye strain. The less, the better.

Some doctors also mention that a higher contrast (either way) is more important than using either mode. And some doctors mention that dark mode can be more damaging for people with astigmatism.

I personally don’t like dark mode at all when using dark background with light text (i.e. black background with white text); I have that annoying feeling of burnt image that takes a minute or so to disappear. And the opposite is also true for some people. And without serious researches about this subject we can only talk by experience.

Now, I think the biggest advantage of dark mode is when you’re using your phone in a low light room, but you would be better off not using your phone right before going to sleep anyway, right?

Browse support

How well supported is dark mode on today’s browsers? To detect whether a browser is running on a dark mode device or not, you’ll need support for (prefers-color-scheme: dark) media query, available on the following devices.

Yeah, I know… this is stupid versioning in action and I shouldn’t have added them anyway. What you need to know is that Safari 13 is very, very new. And so is the support for this media query on every other browser. From the business aspect, it doesn’t make sense to spend your designer’s time on this task, but you’re going to do it anyway, aren’t you? In this case, this is how you can define the CSS.

body {
  color: #333;
  background: #fff;
}

@media screen and (prefers-color-scheme: dark) {
  body {
    background: #2d3239;
    color: #75715e;
  }

  h1 {
    color: #e9d970;
  }
}

You can easily alternate between light and dark mode using Safari’s Web Inspector. I couldn’t find such option on Chrome yet, but we’ll have something similar sooner or later. This example can be found here.

I know what you’re thinking… the CSS can easily get out of hand. And you’re right! Unless you use variables. More specifically, CSS variables.

Creating manageable themes with CSS variables

The trick to organize your CSS is to use variables. To define a variable, you can use the --variable-name: value format.

:root {
  --page-background: #fff;
}

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

That simple. Now, how can we define both of our themes using variables? First, declare your light theme without any media query, like the following:

:root {
  --page-background: #fff;
  --page-title: #333;
  --page-text: #333;
}

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

h1 {
  color: var(--page-title);
}

Now, adding the dark theme is as simple as wrapping the sample properties in a media query. The whole CSS code looks like this:

:root {
  --page-background: #fff;
  --page-title: #333;
  --page-text: #333;
}

@media screen and (prefers-color-scheme: dark) {
  :root {
    --page-background: #2d3239;
    --page-title: #e9d970;
    --page-text: #75715e;
  }
}

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

h1 {
  color: var(--page-title);
}

You can see this example here. You can define any properties to tweak your theme including (but not limited to) background images, borders, shadows, and filters.

Dark mode images and videos

It’s not uncommon to have bright images/videos all over your page. One nice trick is using filters to reduce the brightness of <image> and <video>. You can try a combination of opacity and grayscale filters. Add --image-grayscale and --image-opacity variables and tweak it as you wish:

:root {
  --page-background: #fff;
  --page-title: #333;
  --page-text: #333;
  --image-grayscale: 0;
  --image-opacity: 100%;
}

@media screen and (prefers-color-scheme: dark) {
  :root {
    --page-background: #2d3239;
    --page-title: #e9d970;
    --page-text: #75715e;
    --image-grayscale: 50%;
    --image-opacity: 60%;
  }
}

img,
video {
  filter: grayscale(var(--image-grayscale)) opacity(var(--image-opacity));
}

Image different in a light theme vs dark theme with filters

You can see this example here. This trick won’t work everywhere and may requiring wrapping images in a container. Eventually, you’ll need a different image. That’s where <picture> comes in.

The <picture> element supports media query matchers. So, in case you want to specify a different logo for dark mode, you can use a different <source>. If there are no suitable matches or if the browser doesn’t support the <picture> element, then the default src attribute is selected.

Let’s say that instead of rendering that same image using filters, you wanted to render a totally different darker image.

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

As for the CSS, you can remove everything related to filters. I’ll let this as an exercise for you. The end result can be seen here.

<picture> element in action!

In some cases, you may use an embedded embedded [SVG] and change the colors. It works great for things like flat icons, logos, and that sort of thing, but sometimes you just have to render a different image. Let’s add a logo that can adapt to dark mode.

It’s important to know is that, to style <svg> elements you have to actual render it on your HTML markup. Referencing it through an <img> tag won’t allow any styles on the rendering SVG. With that said, the SVG we’re using looks like this:

<svg
  id="logo"
  width="250px"
  height="55px"
  viewBox="0 0 250 55"
  version="1.1"
  xmlns="http://www.w3.org/2000/svg"
  xmlns:xlink="http://www.w3.org/1999/xlink"
>
  <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
    <g id="logo">
      <path d="..." id="background" fill="#0091FF"></path>
      <path d="..." id="letter" fill="#FFD700" fill-rule="nonzero"></path>
      <path d="..." id="words" fill="#45638B" fill-rule="nonzero"></path>
    </g>
  </g>
</svg>

The colors applied to this SVG are the light mode colors, and by doing that we’re only required to style the dark mode. This is the updated CSS with the SVG styling changes.

@media screen and (prefers-color-scheme: dark) {
  :root {
    --page-background: #2d3239;
    --page-title: #e9d970;
    --page-text: #75715e;

    --logo-background: #4d5866;
    --logo-words: #fff;
  }

  #logo--words {
    fill: var(--logo-words);
  }

  #logo--background {
    fill: var(--logo-background);
  }
}

You can check this example here.

<svg> styling for dark mode

Alternatively, you could have used currentColor as the value of fill and stroke properties. This way, you can change all referenced colors by either setting the SVG’s color property or the inherited color. This approach works extremely well with line icons.

The example above generates a new color every time the button is clicked and sets the <body> element’s color with document.body.style.color = newColor.

Dark mode JavaScript

You may need to do operations that require detecting dark mode as well, like rendering charts. For that you’ll need to use window.matchMedia. The detection is fairly simple.

function isDarkMode() {
  return (
    window.matchMedia &&
    window.matchMedia("(prefers-color-scheme: dark)").matches
  );
}

function renderCanvas() {
  const theme = isDarkMode()
    ? { background: "#4d5866", border: "#7091ba" }
    : { background: "#ffffff", border: "#000" };

  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  const x = 15;
  const y = 15;
  const width = canvas.width - x * 2;
  const height = canvas.height - y * 2;

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.lineWidth = 5;
  ctx.strokeStyle = theme.border;
  ctx.strokeRect(x, y, width, height);
  ctx.fillStyle = theme.background;
  ctx.fillRect(x, y, width, height);
}

renderCanvas();

As you can see, all you have to do is conditionally define your theme once your media matcher detects the dark mode. You can see this example here.

Now, if we switch from one mode to the other (you may also have configured your computer to automatically do it for you), you would see the wrong colors being rendered. To re-render the canvas we can use MediaQueryList.addListener to respond to the change.

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

function isDarkMode() {
  return darkModeMatcher && darkModeMatcher.matches;
}

function onDarkModeChange(callback) {
  if (!darkModeMatcher) {
    return;
  }

  darkModeMatcher.addListener(({ matches }) => callback(matches));
}

function renderCanvas(useDarkTheme) {
  const theme = useDarkTheme
    ? { background: "#4d5866", border: "#7091ba" }
    : { background: "#ffffff", border: "#000" };

  const canvas = document.querySelector("canvas");
  const ctx = canvas.getContext("2d");
  const x = 15;
  const y = 15;
  const width = canvas.width - x * 2;
  const height = canvas.height - y * 2;

  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.lineWidth = 5;
  ctx.strokeStyle = theme.border;
  ctx.strokeRect(x, y, width, height);
  ctx.fillStyle = theme.background;
  ctx.fillRect(x, y, width, height);
}

renderCanvas(isDarkMode());
onDarkModeChange(renderCanvas);

This will make sure the function renderCanvas is called whenever the mode changes. You can see this example here.

And that pretty ends what you need to know about the technical aspects of supporting dark mode in the web.

Wrapping up

Dark mode is the latest trend in tech, not doubt about it. And even without having any serious research backing its so-called “dark mode benefits”, you may consider doing it for the sake of your customers’ happiness. The technical aspects are quite simple, but don’t fool yourself: creating dark themes is a very challenging process, specially when it comes to art direction for all assets (including images and video).

Another thing to consider is whether you should automatically switch to dark mode or use a configurable setting on your site. The latter is very simple to implement by setting a class on an element (e.g. <html data-theme="dark-mode">), but you’d have to manually execute scripts and change images/videos in case you switched from one mode to the other. Either that, or not bothering at all, which would be (probably) fine in most cases.