Posted on

Add Dark Mode to your WordPress Theme

Add a switch to your WordPress website so users can choose between Light Mode and Dark Mode. We’ll use a cocktail of JavaScript/jQuery, PHP and CSS to make it look slick. Building software that uses multiple technologies like this can be a bit of a juggling act. We’ll go through the process step-by-step and we’ll end up with a front-end switch just like this one:

You can include the switch on a page by using a shortcode, or you can stick it in your site’s footer so it’s available on every page.

You’ll need to make sure your site’s using a custom child theme, and we’re going to structure things to keep the theme’s functions.php file clean. There’s a lot to take in here, but the logic’s quite straightforward – it’s the fact we’ve got PHP/JS/CSS that makes it look complicated.

Show no fear and start by breaking it down into little pieces and making a plan.

Make a Plan

  1. We want the actual dark/light CSS to be simple. When the user chooses dark or light mode, we’ll add the “dark-mode” or “light-mode” class to the page’s body element.
  2. We can’t just serve the HTML with these css classes against the body element, because our website probably uses full-page caching. We could append something to the URL, like “/some-url/?dark-mode=1” but that’s not very elegant.
  3. When the user toggles the switch, we’ll use JavaScript/jQuery to adjust the body element’s CSS classes in the browser. Then we’ll set a cookie so that the next time the user fetches a page, we’ll know the user’s choice by checking the cookie.
  4. If the user is logged-in then we won’t be serving cached pages – we’ll be generating the HTML on every page-load. So for logged-in users we can inject the CSS classes server-side to make things a bit more slick.

Getting Started

The first thing you’re going to want to do is set up the CSS for your dark mode. Create two new CSS files in your custom child theme. Here’s the dark-mode file, theme-mode-dark.css.

/**
 * Dark Mode
 */

body.dark-mode {
	background-color: black;
	color: white;
}

…and here’s theme-mode-light.css to get you started.

/**
 * Light Mode
 */

body.light-mode {
	background-color: white;
	color: black;
}

Assuming that you’re site’s in “light mode” before you even start, your theme-mode-light.css file will actually be empty (or nearly empty). You’ll put most of your changes/overrides into theme-mode-dark.css.

We’ll also need a place to put the PHP code, so create a file called functions-theme-mode.php like this…

<?php

/**
 * FILE: functions-theme-mode.php
 */

// Block direct access.
if (!defined('WPINC')) {
	exit('Do NOT access this file directly.');
}

function theme_mode_enqueue_scripts() {
	$theme_modes = array('light', 'dark');

	$stylesheet_directory_uri = get_stylesheet_directory_uri();

//	wp_enqueue_style('theme-mode-css', $stylesheet_directory_uri . '/theme-mode-switch.css', false, '20201108-01');
//	wp_enqueue_script('js-cookie', 'https://cdn.jsdelivr.net/npm/js-cookie@rc/dist/js.cookie.min.js', false, '3.0.0');
//	wp_enqueue_script('theme-mode-js', $stylesheet_directory_uri . '/theme-mode.js', array('jquery', 'js-cookie'), '20201108-01');

	foreach ($theme_modes as $theme_mode) {
		$file_name = 'theme-mode-' . $theme_mode . '.css';
		if (is_readable(trailingslashit(dirname(__FILE__)) . $file_name)) {
			wp_enqueue_style('theme-mode-' . $theme_mode . 'css', $stylesheet_directory_uri . '/' . $file_name, false, '20201108-01');
		}
	}
}
add_action('wp_enqueue_scripts', 'theme_mode_enqueue_scripts');

You’ll see we’ve commented-out a couple of lines where we’re enqueuing the styles & scripts. We’ll come back to those in a moment.

Now you just need to reference the functions-theme-mode.php file from your custom child theme’s main functions.php file, by adding something like this to it…

// Enable Dark/Light mode.
require_once 'functions-theme-mode.php';

The Toggle Switch

It’s always satisfying to get something onto the screen so we can play with it and see our code actually doing something, so let’s get the little toggle switch working first. There are loads of ways of going this, but underneath it all there’s just a regular HTML input[type=”checkbox”] element. Making it look nice is just case of adding some funky CSS mojo.

Before we can style the HTML, we need to actually render the it to a page. We’re going to do this by making a PHP function that can either be called directly, or used as a shortcode. Paste the following into your functions-theme-mode.php file… after the stuff where we queue the scripts.

function do_shortcode_theme_mode_toggler($params = '') {
	global $theme_togger_counter;

	if (!isset($theme_togger_counter)) {
		$theme_togger_counter = 0;
	}
	$theme_toggler_id = 'theme-toggler-' . $theme_togger_counter;

	$icon_light = 'sun-o';
	$icon_dark = 'moon-o';

	$classes = '';
	if (isset($params['class'])) {
		$classes .= $params['class'];
	}

	$styles = '';
	if (isset($params['align']) && ($params['align'] == 'center')) {
		$styles .= 'margin-left: auto; margin-right: auto;';
	}

	if (isset($params['size'])) {
		$styles .= sprintf(' font-size: %s;', esc_attr($params['size']));
	}

	$props = '';
	if (!empty($styles)) {
		$props .= sprintf('style="%s"', $styles);
	}

	$html = '';
	$html .= sprintf('<div class="theme-toggler-outer %s" %s>', $classes, $props);
	$html .= sprintf('<span class="checkbox-label checkbox-label-1"><i class="fa fa-%s" aria-hidden="true"></i></span>', $icon_light);
	$html .= '<div class="checkbox-container">';
	$html .= sprintf('<input id="%s" type="checkbox" />', $theme_toggler_id);
	$html .= sprintf('<label for="%s" title="%s"></label>', $theme_toggler_id, esc_attr__('Toggle theme', 'headwall'));
	$html .= '</div>'; // .checkbox-container
	$html .= sprintf('<span class="checkbox-label checkbox-label-2"><i class="fa fa-%s" aria-hidden="true"></i></span>', $icon_dark);
	$html .= '</div>'; // .theme-toggler-outer

	++$theme_togger_counter;

	return $html;
}
add_shortcode('theme_mode_toggler', 'do_shortcode_theme_mode_toggler');

This is the main PHP lump in our project, so we’ll have a quick run-through of the logic.

  1. Our HTML checkbox is going to have an id attribute, and these need to be unique on a page, so every time our function is called we increment a global counter and give our checkbox an id like “theme-toggler-0”, “theme-toggler-1”, etc.
  2. Our custom child theme has access to Font Awesome and we’re using the sun-o and moon-o icons to represent light and dark modes.
  3. $params is just an associative array, which we can either create when we call our function, or WordPress will do it for us if use the [theme_mode_toggler shortcode] in our content.
  4. Then we just output our HTML to the $html string variable.

To get things up and running, you can now create a test page and add the theme_mode_toggler shortcode to it…

[theme_mode_toggler size="20pt" align="center"]

If you view the page you’ll see some lumpy HTML elements with a sun and a moon. To add some style, create a file in your theme folder called theme-mode-switch.css and paste this into it.

.theme-toggler-outer {
    display: table;
    position: relative;
}

.theme-toggler-outer .checkbox-container,
.theme-toggler-outer .checkbox-label {
    display: table-cell;
	position: relative;
}

.theme-toggler-outer .checkbox-label-1 {
    padding: 0 0.5em 0 0;
}

.theme-toggler-outer .checkbox-label-2 {
    padding: 0 0 0 0.5em;
}

.theme-toggler-outer .checkbox-container {
    height: 100%;
    width: 3em;
    margin: 0;
    padding: 0;
    position: relative;
}

.theme-toggler-outer .checkbox-container label {
    background-color: lightgrey;
    border-radius: 0.75em;
    display: block;
    transition: all 0.3s ease-out;
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    z-index: 2;
    cursor: pointer;
}

.theme-toggler-outer .checkbox-container label::after {
    content: ' ';
    background-color: grey;
    border-radius: 50%;
    position: absolute;
    left: 0.25em;
    top: 0.25em;
    transition: transform 0.1s linear;
    width: 1em;
    height: 1em;
    z-index: 3;
}

.theme-toggler-outer .checkbox-container input {
    visibility: hidden;
    position: absolute;
    width: 1px;
    height: 1px;
    z-index: 2;
}

.theme-toggler-outer input[type='checkbox'] + label {
    margin: 0;
}

.theme-toggler-outer .checkbox-container input:checked + label::after {
    transform: translateX(1.5em);
}

.theme-toggler-outer input:checked + label {
    background-color: #505050;
}

Finally, go back to the top of your functions-theme-mode.php file and uncomment the line that’s trying to queue the “theme-mode-switch.css” file.

importantDon’t uncomment the two wp_enqueue_script() lines yet. We’ll come back to those.

Now if you view the test page with your shortcode on it you should see a nicely rendered toggle switch. It’s a start!

Make the Toggle Switch Do Something

Now we’re going to add some logic to our switch so it actually does some stuff. This is where we’re going to use JavaScript & jQuery to do the browser-based bits. We’ve used…

$(window).on('load', function() { ... 

….instead of…

$(window).load(function(){ ...

…so we can upgrade the stock jQuery that ships with WordPress to version 3, if we want to.

Add a new file called theme-mode.js to your custom child theme, and paste the following into it.

/**
 * theme-mode.js
 */
(function($) {
    'use strict';

    $(window).on('load', function() {
        // Add your own code in here...
        // ...

        var cookieName = 'theme_mode';
        var themeMode = Cookies.get(cookieName);

        if ((themeMode === 'dark')) {
            setDarkMode();
        } else if ((themeMode === 'light')) {
            setLightMode();
        } else {
            // Cookie hasn't been set yet, or theme_mode is invalid.
        }

        function setDarkMode() {
            $('body').removeClass('light-mode');
            $('body').addClass('dark-mode');
            $('.theme-toggler-outer input[type="checkbox"]').prop("checked", true);
            Cookies.set(cookieName, 'dark', { expires: 365, sameSite: 'strict' });
        }

        function setLightMode() {
            $('body').removeClass('dark-mode');
            $('body').addClass('light-mode');
            $('.theme-toggler-outer input[type="checkbox"]').prop("checked", false);
            Cookies.set(cookieName, 'light', { expires: 365, sameSite: 'strict' });
        }

        $('.theme-toggler-outer input[type="checkbox"]').change(function() {
            if (this.checked) {
                setDarkMode();
            } else {
                setLightMode();
            }
        });
    });
})(jQuery);

This is the front-end browser-based script, and it works like this:

  1. Wait for the window to become “loaded”.
  2. Get our custom “theme_mode” cookie. If it’s our first time to the site then the cookie won’t exist yet. We’re using the third-party js-cookie library to make cookie-interaction easy.
  3. If the cookie is ‘dark’ or ‘light’ then call the relevant function to set our switch to the correct position, and add/remove the relevant classes to the document’s body element.
  4. Start listening for the “change” event on every instance of our checkbox, and call the relevant light/dark function whenever the checkbox changes value.

infoWhen the user goes to any page on our website, the theme_mode cookie will be sent to the server by our browser, and the server will send it back to the browser so we can go back to the first step. Cookies are neat 🍪 🍪 🍪

To activate this logic, go back up to the top of functions-theme-mode.php and uncomment the two lines that enqueue the JavaScript files.

Now reload your test page and…

…it should be working!

Finishing Off

If you’ve whizzed through this tutorial by skim-reading and copy-and-pasting then you’re theme-mode-dark.css file will be kinda empty. You’ll definitely want to spend some time getting this file right. If you’re using a lot of plugins like WooCommerce then it’ll take a while to sort out the details.

There’s also a nice little extra we can add. When a user is logged-in, we can look at our theme_mode cookie server-side and inject the right class into the body tag when we serve the HTML. Add the following to the end of your functions-theme-mode.php file.

/**
 * The only time we'll even inject anything other than theme-light is if the
 * user is logged-in and they'ce got 'dark' set in their theme_mode cookie.
 */
function theme_mode_body_class($classes) {
	$theme_mode = 'light-mode';

	if (is_user_logged_in() && isset($_COOKIE['theme_mode']) && ($_COOKIE['theme_mode'] == 'dark')) {
		$theme_mode = 'dark-mode';
	}

	if (!empty($theme_mode) && !in_array($theme_mode, $classes)) {
		array_push($classes, $theme_mode);
	}

	return $classes;
}
add_filter('body_class', 'theme_mode_body_class');

All this does is use the standard WordPress body_class filter to inject either “dark-mode” or “light-mode”. If the user isn’t logged-in then we’ll always inject “light-mode”, because it’s the default mode for the website.

We have to check that the user is logged-in before injecting “dark-mode” into the body classes, otherwise we might end up having a mixture of light-mode and dark-mode pages in our page cache, which would make our website look weird.

Extending the Code

There are all sort of opportunities with this bit of code. You could certainly wrap it up into a plugin, although I’m not sure what you’d gain because it’s very theme-specific.

Maybe you could move away from having just “Theme Type A” and “Theme Type B”, to having a customisable array of colour schemes. Perhaps even editable using the WordPress Customizer.

You could easily create a widget that renders the toggle switch, or you can call it from your footer are like we do on this site – so it’s on every page.

Let me know if you come up with some interesting use cases, and I hope you enjoy working in the dark!

Leave a Reply

Your email address will not be published. Required fields are marked *