How to Dark Mode


Dark Mode is now available on iOS and macOS and turns out to be a super useful new feature. It makes using your devices much easier at night and since it transitions between light and dark automatically based on sunset and sunrise you never have to think about it.

Unfortunately, not a lot of apps have embraced Dark Mode and once you start using it extensively it becomes clear that there are even less websites that do so.

While you might have to wait for your favorite website or app to finally go dark and stop blitzing you with white screens in the middle of the night, you might as well get your own websites ready now.

At first, this sounds super complex, but it turns out to be incredibly easy. At least by the standards of troubles cool new features usually cause.

All of the Light Mode to Dark Mode transitions can be handled via CSS and once set up will switch automatically on the user’s device.

Here is a WordPress theme called Visual Diary which we have developed and have recently updated to work with Dark Mode.

Marcello's website in light mode

The CSS on this theme looks something like this:

body, html { background-color: white; color: black; } a { color: blue; } a:hover { color: red; }
Code language: CSS (css)

All we have to do now, is add an @media declaration called prefers-color-scheme: dark and adjust the elements we want to change accordingly.

@media (prefers-color-scheme: dark) { body, html { background-color: black; color: white; } a { color: red; } a:hover { color: blue; } }
Code language: CSS (css)

Which will make the website look like this once the user switches to Dark Mode.

Marcello's website in dark mode

This is our complete CSS code:

body, html { background-color: white; color: black; } a { color: blue; } a:hover { color: red; } @media (prefers-color-scheme: dark) { body, html { background-color: black; color: white; } a { color: red; } a:hover { color: blue; } }
Code language: CSS (css)

That’s it. It’s like magic!

To preview the changes and to see how the transition will look on the user’s device, we can manually switch between Dark and Light Mode in macOS system preferences.

Transition of light to dark mode on Marcello Curto's website

You can find the entire code for this custom theme on GitHub.

Going wild

Obviously, if your website is more complex, there will be a bit more work involved. We have recently reworked our own WordPress based website to fully embrace Dark Mode.

On our website, every post can have its own background color, which makes it quite colorful, but also a challenge when we want to get every post ready for Dark Mode.

Our website work overview in Light Mode

To enable this granular control over background colors we use the plugin Advanced Custom Field. We created two color picker fields that we can modify in the editor.

Originally, we have then queried the color values from these fields in PHP and added them to the inline CSS of every post wrapper. We had the following code in our index.php and singular.php:

<article <?php post_class( 'post-wrapper' ); ?> id="post-<?php the_ID(); ?>" style=" <?php echo 'background-image:linear-gradient(to ' . get_field( 'direction_color' ) . ', ' . get_field( 'background_color_left' ) . ', ' . get_field( 'background_color_right' ) . ');'; ?>" >
Code language: PHP (php)

Before we adjust that code, the first step now is to expand the color selectors to include two more colors which we’ll use for the dark background colors.

Advanced Custom Fields jQuery Color Selectors

Since it’s not possible to put @media (prefers-color-scheme: dark) inline we have to update our code quite a bit. Inside our WordPress post loop we’ll add:

<?php $id_of_post = get_the_ID(); $direction_color = get_field( 'direction_color' ); $field_light = get_field( 'light_colors' ); $left_light = $field_light['background_color_left']; $right_light = $field_light['background_color_right']; if (empty($left_light)) { $left_light .= '#ffffff00'; } if (empty($right_light)) { $right_light .= '#ffffff00'; } $field_dark = get_field( 'dark_colors' ); $left_dark = $field_dark['background_color_left']; $right_dark = $field_dark['background_color_right']; if (empty($left_dark)) { $left_dark .= '#ffffff00'; } if (empty($right_dark)) { $right_dark .= '#ffffff00'; } $custom_css_light .= '#post-' . $id_of_post . ' {background-image:linear-gradient(to ' . $direction_color . ', ' . $left_light . ', ' . $right_light . ')} '; $custom_css_dark .= '#post-' . $id_of_post . ' {background-image:linear-gradient(to ' . $direction_color . ', ' . $left_dark . ', ' . $right_dark . ')} '; ?>
Code language: PHP (php)

This calls all the Advanced Custom Field variables that we have defined and adds them to a CSS string. This string also contains the specific post ID we need to target the right container, since our <article> wrapper has an id that contains the post ID.

To make use of @media queries, this code would have to go into a CSS Stylesheet that we enqueue in our functions.php, that then gets called when the page loads wp_head.

However, by the time WordPress loops through all the individual posts, wp_head has already been called and it’s too late to read the color values we have set. Luckily, there is a neat solution that WordPress provides to add stylesheets at a later time. We’ll add the following code just before we call get_footer:

<?php if ( !empty($custom_css_light) || !empty($custom_css_dark) ) { add_action( 'custom_style_action', 'roark_enqueue_post_css', 10, 1 ); $custom_css = $custom_css_light . '@media (prefers-color-scheme: dark) {' . $custom_css_dark . '}'; function roark_enqueue_post_css( $css ) { wp_register_style( 'roark-custom-post-style', false ); wp_enqueue_style( 'roark-custom-post-style' ); wp_add_inline_style( 'roark-custom-post-style', $css ); } do_action( 'custom_style_action', $custom_css ); } ?>
Code language: PHP (php)

This will add a <style> element inside <body>, which is neither elegant nor correct according to W3C standards, but will work in most browsers anyway.

An easy way to remedy this is by letting a plugin like Autoptimize optimize your site’s CSS. The plugin will automatically add the CSS to the main <style> element within <head>. Compliance restored.

After setting dark color alternatives for each post, our website now looks like this in Dark Mode.

Our website work overview in Dark Mode


On your site, some plugins might also enqueue their own stylesheets that are most likely not setup to work with dark mode. In our case, the brilliant plugin Syntax-highlighting Code Block (with Server-side Rendering) by Weston Rutter, which formats code in the Gutenberg Code Block, was one that needed some adjustments. This plugin loads it’s own stylesheet, so the first thing we have to do is dequeue it in functions.php of our theme:

add_action('wp_footer', function() { wp_dequeue_style('syntax-highlighting-code-block'); });
Code language: PHP (php)

Conveniently the plugin comes with a lot of code color themes, many of which already include a light and a dark version. All we have to do is combine them in one file and add that file to our theme stylesheet. Our favorites are Atom One Light & Atom One Dark by Daniel Gamage.

Because we move it into our theme SCSS stylesheet, we can adjust it to use the color variables already in use in our main SCSS file, which makes keeping track of future color changes much easier. The final SCSS file looks like this:

$color-white: #ffffff; $color-background-dark: #2a3d46; .hljs { display: block; overflow-x: auto; padding: 0.5em; color: #383a42; background: $color-white; } .hljs-comment, .hljs-quote { color: #a0a1a7; font-style: italic; } .hljs-doctag, .hljs-keyword, .hljs-formula { color: #a626a4; } .hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst { color: #e45649; } .hljs-literal { color: #0184bb; } .hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string { color: #50a14f; } .hljs-built_in, .hljs-class .hljs-title { color: #c18401; } .hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number { color: #986801; } .hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title { color: #4078f2; } .hljs-emphasis { font-style: italic; } .hljs-strong { font-weight: bold; } .hljs-link { text-decoration: underline; } @media (prefers-color-scheme: dark) { .hljs { color: #abb2bf; background: $color-background-dark; } .hljs-comment, .hljs-quote { color: #5c6370; } .hljs-doctag, .hljs-keyword, .hljs-formula { color: #c678dd; } .hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst { color: #e06c75; } .hljs-literal { color: #56b6c2; } .hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string { color: #98c379; } .hljs-built_in, .hljs-class .hljs-title { color: #e6c07b; } .hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number { color: #d19a66; } .hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title { color: #61aeee; } }
Code language: SCSS (scss)

The result is a neat transition from light code to dark code.

Code block transition from light to dark

Dark Images

All of this is great, but it would be even cooler to have two versions of at least some images we can use interchangeably depending on whether the user is in Dark or Light Mode. Turns out this is also easier than expected thanks to the HTML5 picture tag. Browser support is pretty decent, except for Internet Explorer, but you’ll always have a fallback anyhow.

We’ll have to load our main image and the alternative image within the picture tag and provide the main image as a fallback. The nice part is that the source tags accept media queries so we don’t have to mess with JavaScript or external stylesheets. We could just use a version of the following code and would be done:

<picture> <source media="(prefers-color-scheme: dark)" srcset="dark.jpg"> <img src="light.jpg"> </picture>
Code language: HTML, XML (xml)

But for our website we will want our images to be responsive, which WordPress takes care of by generating srcset attributes to let the browser know in which size to serve the image to the user. An img tag usually looks like this:

<img src="" class="attachment-medium size-medium wp-post-image wp-post-image wp-post-image wp-post-image wp-post-image" alt="Fernsehturm in Munich" srcset=" 2000w, 150w, 400w, 768w, 1200w" >
Code language: HTML, XML (xml)

However, before we figure out how to get a responsive alternative version of the above, we first have to add another Advanced Custom Fields image field like this:

Creating an image field in Advanced Custom Fields

Make sure to also set the Return Format as Image ID, because that’s what we’ll need once we call the field. This is what our new picture tags will look like:

<picture> <?php $image_sizes = 'large'; $image_dark = get_field('night_image'); if( $image_dark ) { $srcset_dark = wp_get_attachment_image_srcset( $image_dark, $image_sizes); echo '<source media="(prefers-color-scheme: dark)" srcset="' . $srcset_dark . '">'; } echo get_the_post_thumbnail( null, $image_sizes ); ?> </picture>
Code language: HTML, XML (xml)

After we get the Advanced Custom Field image via get_field, we check whether it actually is set. If so, retrieve the srcset string that WordPress provides for the image. The WordPress function wp_get_attachment_image_srcset () makes this super easy. Then we add those values to the source tag that gets called when the user is in Dark Mode.

The picture tag also requires an img element. This gets called automatically when we echo the get_the_post_thumbnail() function. The img tag also acts as a fallback image, should the user’s browser not support the picture element. Now, it will either load the Light or Dark Mode image and only when the users switches does it load the second one, hence your server doesn’t have to serve an unnecessary image and waste resources.

With a bit of Photoshop Magic we can even transform some photos into night time shots.

And that’s that. At least in terms of code, going Dark Mode is incredibly easy. The hard part is how to make meaningful changes to the design, because oftentimes just inverting white to black and vice versa will not suffice.

We have uploaded the source code of this theme on GitHub, so it will hopefully be easier to understand how all this works in the context of other theme files.

Let us know in the comments how this works for you. If you need help implementing this on your own website feel free to contacts us!

Published on:

October 27th 2019