The Breakroom

Dark Mode and CSS

October 25, 2018

By Craig Hockenberry

The new Dark Mode in macOS Mojave provides users with a new way to customize their desktop environment. It also presents a lot of new situations where developers need to adapt content in their apps and websites.

While these changes are currently limited to native macOS apps, the arrival of Marzipan next year makes it likely that iOS developers will be confronted with similar changes in UIKit. A dark user interface would be a reality on all of Apple’s platforms, including tvOS and watchOS.

These changes also ripple out to web developers who are creating apps or site content that can look out of place when surrounded with dark interface elements. Some sites are using switchable themes: MacStories is a great example (use the contrast icon in the upper-right corner.)

So let’s take a moment to see where things are now, where they’re headed in the near future, and look at strategies for adapting content presentation.

Code Changes

Dark Mode has been on developers’ minds since its introduction at WWDC 2018. And since it’s an extension of the dark and light AppKit appearances that debuted in Yosemite, the consequences of this new user interface are already well understood.

For more information on what these changes mean from a Mac developer’s point of view, I recommend Dark Side of the Mac by Kuba Suder and Supporting Dark Mode by Daniel Jalkut.

Both these series cover the changes you’ll be making in the views, controls, and images throughout your app. UIKit developers can consider this information a preview of WWDC 2019 :-)

Enter WebKit

Apps on macOS have been able to embed WebKit since its introduction. Adding web content to native apps was one of its major design goals: an app called Safari was the the first one to use the new framework.

Over the years many developers, both on macOS and iOS, have taken advantage of this framework for one simple reason: HTML and CSS are a great combination for presenting content.

So it would make sense that all this cool new Dark Mode stuff would be available in WebKit, right? Nope, and making it happen is more difficult than you’d initially suspect.

Sometimes You Feel Like a Nut

Web content has to exist in many different contexts. In applications you want content that matches the surrounding native frame. With apps like the App Store, it’s hard to tell where the native code ends and the web code starts. In this situation, you want web content to respect the user’s theme choice in System Preferences.

The last post in Daniel’s series addresses the issue of adapting WebKit to an app’s needs. You’ll see how he’s watching for system changes and adapting the markup for an about box.

While this proposed solution works great for a small amount of markup, injecting code into HTML won’t scale well for richly styled content. His approach also makes it difficult for a designer: they can’t work directly with HTML and CSS files.

Sometimes You Don’t

This adaptability has a downside for the majority of web content: the environment created inside the main browser frame usually exists on its own and wants to ignore Dark Mode. For example, you wouldn’t want a dark form control to suddenly appear on Wikipedia because of a system preference change. And where is the boundary between content and chrome: should a scrollbar color match the markup or the browser where it’s presented?

Before you answer, consider these diverse viewpoints. The question is simple, the answer is not.

A New Standard

Luckily the standards groups have been debating this issue for the past several months and have settled on a new media query called prefers-color-scheme. It’s not available in any shipping browser yet, but thanks to the WebKit team, it just landed in the new Safari Technology Preview.

After downloading Release 68 and making sure that Dark Mode CSS Support is turned on in the Develop > Experimental Features menu, you can do this in your CSS:

div {
  background-color: pink;
}

@media (prefers-color-scheme: light) {
  div {
    background-color: yellow;
  }
}

@media (prefers-color-scheme: dark) {
  div {
    background-color: purple;
  }
}

As you switch in and out of Dark Mode, the div will change color. If you need a more interactive approach, say to show a theme-switching control at page load, you can use JavaScript to check the media query string:

var inDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;

A related standard for supported-color-schemes is still being worked out. The idea behind this new meta tag is to give authors control over how colors switch automatically for form controls, scrollbars, and system colors like ButtonText. Its current implementation in WebKit looks like this:

<meta name="supported-color-schemes" content="light dark">

Whether this stays as a meta tag or becomes a CSS property is still being debated. It’s definitely something you want to keep an eye on if you use native controls on your site.

With these changes, Dark Mode now becomes another aspect of responsive web design. As with device dimensions or color, the user’s environmental choices must be taken into account. Instead of adapting elements to viewport changes, you’ll be changing designs to match the user interface chrome outside that viewport.

Semantic Style

The problem now becomes logistical: there are a lot of color and image properties in our CSS. Do you really want to duplicate all those definitions for a dark theme? Or make changes in two places? Hell no.

The solution is to start thinking about color differently.

One of the things that made the switch to Dark Mode easier in macOS was its semantic treatment of color. There are variables like labelColor, windowBackgroundColor, and controlTextColor used to represent color values. Without knowing anything about macOS development, you can probably guess what these definitions look like. You’ve just started thinking semantically.

When you’re dealing with light and dark variations of a color, it’s a lot easier to think of “text color” and not worry about whether it’s currently rgb(0,0,0) or rgb(255,255,255). It also helps when you’re dealing with a site’s branding: I can remember “Iconfactory branding color” but never rgb(229,36,30).

This, of course, can be extended to images. If you’ve got a button image that changes depending on context, just call it “refresh icon” and forget about the url().

Variables to the Rescue

Luckily, a new CSS feature that’s starting to see wide browser adoption is available to make this dream a reality. Hello CSS variables.

There are a lot of options, but start with the :root pseudo-element as a container for your variables. This is what it looks like:

:root {
  --branding-color: rgb(229, 36, 30);
}

And once you have that variable defined, you can use it anywhere in your CSS:

header {
  background-color: var(--branding-color);
}
footer {
  background-color: var(--branding-color);
}

You’re not limited to using just colors, either. These variables can be used for font lists, URLs, and any other property type.

Pretty awesome, especially when marketing decides they want to change your branding color, logo, and font face!

Picking Out the Pieces

There’s nothing preventing you from having more than one container for your variables, so we can extend this facility to support themes. First, we’ll use two CSS files. One called dark.css:

:root {
  --text-color: white;
  --page-background-color: black;
}

And another called light.css where we switch the colors around:

:root {
  --text-color: black;
  --page-background-color: white;
}

In a shared.css file, you use these definitions as needed:

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

To switch themes, you tell the DOM which theme file to use. You might do this server-side by looking at a preference setting in a cookie and emitting the right <link> element. But the user experience is probably better if you use JavaScript to manage the stylesheet:

<link id="theme" href="style/light.css" ... />

<script type="text/javascript">
  function switchThemes() {
    var e = document.getElementById('theme');
    if (e.getAttribute('href') == 'style/light.css') {
      e.setAttribute('href', 'style/dark.css');
    }
    else {
      e.setAttribute('href', 'style/light.css');
    }
  }
</script>

<button onclick="switchThemes();">Switch</button>

If needed, the browser can switch themes according to the user’s system preference. When the media query becomes available, use it to wrap your variable definitions and you’re good to go:

@media (prefers-color-scheme: light) {
  :root {
    --text-color: black;
    --page-background-color: white;
  }
}
@media (prefers-color-scheme: dark) {
  :root {
    --text-color: white;
    --page-background-color: black;
  }
}

Note that if you need to support themes on older browsers, you can use tools like Sass or Less to accomplish something similar. Variables are used to generate multiple CSS files which can then be switched using the <link> URL.

In our experience, the two hardest parts of all this are coming up with semantic names and colors that work well in both dark and light settings. It’s more of a design problem than a technology issue. If you need help, we’re here to help. We’ve been dealing with these issues for years, both in apps and on the web, and want to share our experience making award-winning products.

Adding Polish

This is already pretty great, but we can make it even sexier with CSS animation. In the browser it adds a level of finish; in an app it lets you match the animation in AppKit layers.

To make the job easier, we’ll again rely on CSS variables. Making the animation configurable ensures that animations stay in sync and lets you experiment or debug with ease. Here’s an example of the variables you might use:

:root {
  --duration: 1.0s;
  --timing: ease-in-out;
}

And they’re used to animate color changes in the document:

body {
  transition: color var(--duration) var(--timing),
              background-color var(--duration) var(--timing);
}

Note that transitions only apply to the element where they’re defined or inherited. If you set colors on another element, like a <button>, you’ll need to create a transition on that element, too. Thanks to color variables that start with two dashes, it’s easy to search for them in your markup.

Pesky Scrollbars

This whole treatise got its start with some Halloween decorations. Every year we do something fun at the Iconfactory and this year I noticed a problem. The scrollbars looked bad in the new macOS Dark Mode, so I worked around it with CSS to make them scary.

Scrollbars have been customizable in WebKit for almost 10 years. Unfortunately, this mechanism never saw standards acceptance and has retained the “-webkit-” vendor prefix.

The ::-webkit-scrollbar pseudo-elements used to configure the scrollbars still work in Chrome and Safari, but they’re buggy. They don’t animate along with the content and don’t hide when the user has a trackpad or other physical scrolling mechanism. I don’t recommend using them, and neither does the CSS working group:

The WebKit implementation of pseudo-elements for scrollbar is considered to be a feature mistakenly exposed to the web.

After deploying our decorations, I showed one of the members of the WebKit team the screenshot below and asked what was going on.

The white-belt-with-dark-pants look at Daring Fireball is something being worked on – some of the changes in WebKit require an update to the scrollbars that AppKit provides. This problem can only be addressed by a macOS update.

My earlier question about who owns the scrollbars at the boundary between web content and a native app is indeed a difficult one.

The good news is another new standard called scrollbar-color has been proposed. It includes four choices: auto, dark, light, and custom. The choice of auto lets the browser decide what’s best, or you can use light and dark to adapt them to the content you’re presenting.

The custom option lets you specify two colors: the first is for the scrollbar thumb, the second is for the track. There’s also a scrollbar-width in the proposal that allows you to size the scrollbar. Here’s an example using CSS variables:

section div.container {
  scrollbar-color: var(--thumb-color) var(--track-color);
  scrollbar-width: var(--width);
}

These features are still in flux, but it’s clear to me that the standards bodies are aware of the problem that web authors are currently facing with scrolling content.

Putting it All Together

We’ve covered a lot of topics in this piece, but these thousands of words are indeed worth a picture: in a simple demo. (You saw a preview in the animation at the top of this post.)

The page itself isn’t remarkable: just a single new meta tag and some simple JavaScript. The good bits are in the CSS file that uses the theme variables. There are a lot of comments and links, so take your time going over the source code.

As always, remember that we’re here to help, either with design or development services. We love making great looking products, no matter if they’re dark or light!