Dark mode

I had a very productive time at Indie Web Camp Amsterdam. The format really lends itself to getting the most of a weekend—one day of discussions followed by one day of hands-on making and doing. You should definitely come along to Indie Web Camp Brighton on October 19th and 20th to experience it for yourself.

By the end of the “doing” day, I had something fun to demo—a dark mode for my website.

Y’know, when I first heard about Apple adding dark mode to their OS—and also to CSS—I thought, “Oh, great, Apple are making shit up again!” But then I realised that, like user style sheets, this is one more reminder to designers and developers that they don’t get the last word—users do.

Applying the dark mode styles is pretty straightforward in theory. You put the styles inside this media query:

@media (prefers-color-scheme: dark) {
...
}

Rather than over-riding every instance of a colour in my style sheet, I decided I’d do a little bit of refactoring first and switch to using CSS custom properties (or variables, if you will).

:root {
  --background-color: #fff;
  --text-color: #333;
  --link-color: #b52;
}
body {
  background-color: var(--background-color);
  color: var(--text-color);
}
a {
  color: var(--link-color);
}

Then I can over-ride the custom properties without having to touch the already-declared styles:

@media (prefers-color-scheme: dark) {
  :root {
    --background-color: #111416
    --text-color: #ccc;
    --link-color: #f96;
  }
}

All in all, I have about a dozen custom properties for colours—variations for text, backgrounds, and interface elements like links and buttons.

By using custom properties and the prefers-color-scheme media query, I was 90% of the way there. But the devil is in the details.

I have SVGs of sparklines on my homepage. The SVG has a hard-coded colour value in the stroke attribute of the path element that draws the sparkline. Fortunately, this can be over-ridden in the style sheet:

svg.activity-sparkline path {
  stroke: var(--text-color);
}

The real challenge came with the images I use in the headers of my pages. They’re JPEGs with white corners on one side and white gradients on the other.

header images

I could make them PNGs to get transparency, but the file size would shoot up—they’re photographic images (with a little bit of scan-line treatment) so JPEGs (or WEBPs) are the better format. Then I realised I could use CSS to recreate the two effects:

  1. For the cut-out triangle in the top corner, there’s clip-path.
  2. For the gradient, there’s …gradients!
background-image: linear-gradient(
  to right,
  transparent 50%,
  var(—background-color) 100%
);

Oh, and I noticed that when I applied the clip-path for the corners, it had no effect in Safari. It turns out that after half a decade of support, it still only exists with -webkit prefix. That’s just ridiculous. At this point we should be burning vendor prefixes with fire. I can’t believe that Apple still ships standardised CSS properties that only work with a prefix.

In order to apply the CSS clip-path and gradient, I needed to save out the images again, this time without the effects baked in. I found the original Photoshop file I used to export the images. But I don’t have a copy of Photoshop any more. I haven’t had a copy of Photoshop since Adobe switched to their Mafia model of pricing. A quick bit of searching turned up Photopea, which is pretty much an entire recreation of Photoshop in the browser. I was able to open my old PSD file and re-export my images.

LEGO clone trooper Brighton bandstand Scaffolding Tokyo Florence

Let’s just take a moment here to pause and reflect on the fact that we can now use CSS to create all sorts of effects that previously required a graphic design tool like Photoshop. I could probably do those raster scan lines with CSS if I were smart enough.

dark mode

This is what I demo’d at the end of Indie Web Camp Amsterdam, and I was pleased with the results. But fate had an extra bit of good timing in store for me.

The very next day at the View Source conference, Melanie Richards gave a fantastic talk called The Tailored Web: Effectively Honoring Visual Preferences (seriously, conference organisers, you want this talk on your line-up). It was packed with great insights and advice on impementing dark mode, like this little gem for adjusting images:

@media (prefers-color-scheme: dark) {
  img {
    filter: brightness(.8) contrast(1.2);
  }
}

Melanie also pointed out that you can indicate the presence of dark mode styles to browsers, although the mechanism is yet to shake out. You can do it in CSS:

:root {
  color-scheme: light dark;
}

But you can also do it in HTML:

That allows browsers to swap out replaced content; interface elements like form fields and dropdowns.

Oh, and one other addition I added after the fact was swapping out map imagery by using the picture element to point to darker map tiles:

<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.mapbox.com/styles/v1/mapbox/dark-v10/static...">
<img src="https://api.mapbox.com/styles/v1/mapbox/outdoors-v10/static..." alt="map">
</picture>

light map dark map

So now I’ve got a dark mode for my website. Admittedly, it’s for just one of the eight style sheets. I’ve decided that, while I’ll update my default styles at every opportunity, I’m going to preservethe other skins as they are, like the historical museum pieces they are.

If you’re on the latest version of iOS, go ahead and toggle the light and dark options in your system preferences to flip between this site’s colour schemes.

Have you published a response to this? :

Responses

Aaron Parecki

👏 so cool! Some day I will get around to doing this on my website. Great idea using CSS variables too.

Chris M.

“Dark mode” for this site has been one of those things on my todo list for a while. The in-development theme I’m working on has been built from the ground up to support it through the use of media queries and CSS custom properties — but I hadn’t actually implemented it. After a nudge from reading this post by Jeremy I’ve finally implemented something to try out:

For now I’m borrowing Jeremy’s colours, but I’m planning to tweak these as the design evolves.

# Posted by Chris M. on Monday, October 7th, 2019 at 5:17pm

qubyte.codes

That’s right! After more than a year of talking about adding a dark mode I finally did it. The wider support for prefers-color-scheme is what pushed me over the edge. I’m also a slave to fashion.

Initially I wanted to create a sort of dark-mode-battenberg theme, but after a while using dark mode site I came to realise that my favourites are the ones which use as much black as possible. It’s becoming more common to see OLED screens in the wild, and these use less energy for black pixels since OLED pixels are self illuminating. I settled on using black for the background, and borrowing my light mode background colour for text and borders.

The additions to the CSS aren’t too dramatic, but I learnt a few things along the way

@media (prefers-color-scheme: dark) { :root { --background-color-main: #000; --background-color-alt: #000; --standout-color-main: hsl( var(--base-background-hue), var(--base-background-sat), var(--base-background-lum) ); --standout-color-alt: hsl( calc(var(--base-background-hue) - 30), var(--base-background-sat), var(--base-background-lum) ); } img:not([src*=".svg"]) { filter: brightness(.8) contrast(1.2); } pre { background: #000; } code { filter: invert(); }
} body > header { z-index: 1; /* works around stacking context issue introduced by filters */
}

The media query applies the content only when the OS is in dark mode and the browser supports the media query. The :root swaps the background colors to standout colours, and makes the background black.

Non-SVG images have their brightness and contrast adjusted with a filter. I borrowed a dark mode filter from Melanie Richards as suggested by Jeremy Keith.

Code listings are more difficult. I plan to do a bit more on these later, but for now, since the regular code is on a white bacground, I give dark mode code containers a black background and apply an invert filter to the code.

Finally, the filters had an unintended side effect. Filters place the elements they apply to above positioned elements in the stacking context, which led to images and code listings scrolling above the nav bar (which is part of a sticky positioned header). To address this issue I resorted to using z-index. While the rule of thumb seems to be to avoid use of z-index, I believe it’s appropriate in this case.

# Saturday, October 12th, 2019 at 1:30am

john holt ripley

“…one other addition I added after the fact was swapping out map imagery by using the picture element to point to darker map tiles:” <picture> <source media=”prefers-color-scheme: dark” srcset=”“> adactio.com/journal/15941

6 Likes

# Liked by Lucid00 on Monday, October 7th, 2019 at 5:37am

# Liked by Daniel Ehniss on Monday, October 7th, 2019 at 5:37am

# Liked by Michael Spellacy (Spell) on Monday, October 7th, 2019 at 5:37am

# Liked by Dominik Schwind on Monday, October 7th, 2019 at 6:43am

# Liked by Chris Burnell on Monday, October 7th, 2019 at 11:42am

# Liked by Bram de Haan on Monday, October 7th, 2019 at 8:46pm