Skip to content

At Razorfrog, our designers and developers are proponents of accessible choices. We believe that choices represent empowerment, and that a lack of choices can lead to negative outcomes including disengagement. As a web design firm assisting our clients with their accessibility conformance goals and remediation efforts, we recognize that many users browse the web in vastly different ways. From a simplified perspective, accessibility is all about providing multiple ways for users to accomplish the same action or goal.

While working on our new site redesign, we decided to implement a fully accessible custom dark mode on our website. This article discusses how we approached this objective in detail. Before we get too deep into the topic, we’d like to share our toggle solution with you to test out. Go ahead, give it a try below!

Embracing the dark

Dark mode has become increasingly more relevant to how and when many of us browse the modern web. For years concerns have been on the rise around screen time and how screen brightness impacts our health, including our ability to experience restful sleep. This user setting has been receiving substantial attention with the rise of inclusive design and has become a welcome addition to the web’s digital accessibility toolkit.

While the online world has adapted to having a plethora of products and services to choose from, digital interfaces have largely not kept up. Web designers and developers might have insights into how to build digital experiences for users, but no one can claim to know what someone wants at any given moment. That’s why it is valuable to provide users with options based on their immediate preferences and needs.

A high adoption rate

Latina Female Sitting on Couch Reading Tablet at Night

Research confirms the popularity of dark mode and how it has been readily adopted by the vast majority of web users to reduce eye strain, minimize blue light exposure and prolong battery life. According to Earthweb, almost 82% of smartphone users prefer to use dark mode in 2024. This is a major revelation that we learned during Carie Fisher’s talk while attending WordPress Accessibility Day on September 27, 2023.

Carie brought to our attention that dark mode can be accomplished by utilizing the power of the CSS media query @prefers-color-scheme. This allows developers to separately implement styling for light and dark modes. The beauty of this method is that it follows the user’s system settings, allowing the user to always remain in control of the interface that they prefer to utilize at any given time. It’s truly a game-changer for the audiences we are building our websites for!

Most popular applications such as YouTube and Instagram include the option to set dark mode to match your device’s system settings or manually enable it. However, if you browse the web at night using dark mode, chances are you’ll still be greeted by many websites with blinding white backgrounds that may disrupt your sleep cycle. Dark mode does not guarantee websites will be presented with dark themes. This is because each individual site may or may not specify @prefers-color-scheme to include dark mode styling. Sites that don’t are presented in light mode by default as this is the only available option.

Various implementations

Manually configuring your site’s dark mode can be a simple or an intensive process. The most significant factors that determine how much effort dark mode requires include 1) your goals for how to implement it, 2) the framework your website is built with, and 3) your color palette. In most circumstances, the more straightforward your light mode theme is, the easier your dark mode theme will be to prepare.

When approaching your custom dark mode, it’s helpful to think of it as another skin for your website. While not always the case, this often means it is twice the work overall, so do take your current workload into consideration prior to diving into development.

Simple websites

If you’re site uses a black and white color scheme, you can simply use the invert CSS function as mentioned in Inclusive Components’ article “A Theme Switcher.” This method (separate from their theme switch component) is the simplest dark mode approach and takes only 5 minutes to prepare when combined with @prefers-color-scheme.

Community plugins

There are also many plugins available from the wordpress.org repository which can implement a dark mode for you. Most include a variety of settings to assist with customization and styling efforts. However, be aware that many of them inherently have accessibility issues which are counterproductive. Dark mode is often much more complex than installing a plugin since every website is built differently. These are all things to consider with your implementation process.

Razorfrog’s three-part approach

Top Down View of a Man Working on a Silver MacBook Pro at Night

Part 1: Identifying our implementation goals

Prior to getting started, we needed to determine what our primary implementation goals would be for our dark mode. Our team determined these to be two-fold:

  1. Respect the user’s dark mode system settings by default to provide the best possible user experience. We want to prevent the need to constantly enable or disable the dark mode while visiting our site.
  2. Implement a toggle button on the front end of our website to change between light and dark mode via a mouse and a keyboard. It is essential for this toggle to be WCAG conformant based on our site’s accessibility objectives.

Part 2: Developing our implementation strategy

Initially, we aimed to align our dark mode feature exclusively with users’ system preferences through the CSS media query @prefers-color-scheme. However, we quickly realized that omitting a manual toggle option would not fully cater to user needs.

Inspiration struck upon discovering Salma Alam-Naylor’s insightful article, “The Best Light/Dark Mode Theme Toggle in JavaScript.” This resource equipped us with a strategic blueprint to empower users with enhanced control over their theme preference, employing the data-theme property to toggle between [data-theme=“dark”] and [data-theme=“light”], ensuring a comprehensive and user-driven experience.

Further research and experimentation refined our understanding of how to seamlessly integrate dark mode. We concluded that there are three necessary steps for completing our task:

Step 1. Initial Loading Process

Upon visiting the website, we ascertain the dark mode’s activation status in two ways:

  • Detecting the system’s dark mode setting through window.matchMedia(“(prefers-color-scheme: dark)“).
  • Verifying if the user has previously enabled dark mode on our site via localStorage.getItem(“theme”).

Unless the user has explicitly chosen a theme on our site, we defer to the system’s settings. A user’s choice on our website, once made, is prioritized and preserved through cookies, ensuring their preference is respected during subsequent visits.

Step 2. Theme Application

Based on the determination process, we dynamically set the <html> element’s data-theme attribute to either [data-theme=“dark”] or [data-theme=“light”]. This attribute governs the application of theme-specific styles. Additionally, we update the theme toggle buttons to reflect the current mode, maintaining a consistent and intuitive user interface.

Step 3. Toggle Button Placement and Accessibility

We strategically positioned toggle buttons via shortcodes in three locations: the top navigation menu, the mobile navigation menu, and the footer, facilitating effortless theme adjustments from anywhere on the site. Each button is equipped with an event listener for theme toggling, ensuring synchronicity across all toggle buttons.

In order for our dark mode toggle solution to be fully accessible and WCAG conformant, we ensured compatibility with keyboard navigation, the enter/return key, and provided accurate aria-label attributes for assistive technologies.

Below is the final code added to functions.php that includes our three shortcodes and our color scheme detection script:


// Dark Mode Shortcode for Main Navigation Menu.

add_shortcode( 'dark-mode-toggle-nav', 'dark_mode_toggle_nav_shortcode' );
function dark_mode_toggle_nav_shortcode() {
    return '<div class="rf-darkmode-toggle">
        <button data-theme-toggle-nav class="container" aria-label="Light mode is active" title="Toggle color mode">
            <img class="sun visible" src="/wp-content/themes/razorfrog/svg/dark-mode/sun-light-mode-toggle-icon.svg" />
            <img class="moon" src="/wp-content/themes/razorfrog/svg/dark-mode/moon-dark-mode-toggle-icon.svg" />
        </button>
    </div>';
}

// Dark Mode Shortcode for Mobile Navigation Menu.
add_shortcode( 'dark-mode-toggle-mobile', 'dark_mode_toggle_mobile_shortcode' );
function dark_mode_toggle_mobile_shortcode() {
    return '<div class="mobile-menu-dm-toggle">
        <div class="dm-toggle-text">
            <p>Toggle light/dark mode</p>
        </div>
        <div class="dm-toggle-switch">
            <div class="rf-darkmode-toggle">
                <input data-theme-toggle-mobile type="checkbox" id="dark-mode-toggle-mobile" title="Toggle color mode" />
                <label for="dark-mode-toggle-mobile">
                    <img class="sun" src="/wp-content/themes/razorfrog/svg/dark-mode/sun-light-mode-toggle-icon.svg" />
                    <img class="moon" src="/wp-content/themes/razorfrog/svg/dark-mode/moon-dark-mode-toggle-icon.svg" />
                </label>
            </div>
        </div>
    </div>';
}

// Dark Mode Shortcode for Footer.
add_shortcode( 'dark-mode-toggle-footer', 'dark_mode_toggle_footer_shortcode' );
function dark_mode_toggle_footer_shortcode() {
    return '<div class="rf-darkmode-toggle">
        <input data-theme-toggle-footer type="checkbox" id="dark-mode-toggle-footer" title="Toggle color mode" />
        <label for="dark-mode-toggle-footer">
            <img class="sun" src="/wp-content/themes/razorfrog/svg/dark-mode/sun-light-mode-toggle-icon.svg" />
            <img class="moon" src="/wp-content/themes/razorfrog/svg/dark-mode/moon-dark-mode-toggle-icon.svg" />
        </label>
    </div>';
}

// Dark Mode Color Scheme Detection Script.
add_action('wp_footer', 'color_scheme_detection_script_v1');
function color_scheme_detection_script_v1() {
    ?>
    <script>
    /* 
    * Utility function to calculate the current theme setting.
    */
    function calculateSettingAsThemeString({ localStorageTheme, systemSettingDark }) {
        if (localStorageTheme !== null) {
            return localStorageTheme;
        }
        
        if (systemSettingDark.matches) {
            return "dark";
        }
        
        return "light";
    }
    
    /*
    * Utility function to update the button text and aria-label.
    */
    function updateButton({ buttonEl, isDark }) {
        const newCta = isDark ? "Dark mode is active" : "Light mode is active";
        
        // use an aria-label if you are omitting text on the button
        // and using a sun/moon icon, for example
        buttonEl.setAttribute("aria-label", newCta);
        buttonEl.checked = isDark ? true : false;;
        
        navSun.classList.toggle('visible');
        navMoon.classList.toggle('visible');
    }
    
    /*
    * Utility function to update the theme setting on the html tag.
    */
    function updateThemeOnHtmlEl({ theme }) {
        document.querySelector("html").setAttribute("data-theme", theme);
    }
    
    /*
    * On page load:
    */
    
    /*
    * 1. Grab what we need from the DOM and system settings on page load.
    */
    const button1 = document.querySelector("[data-theme-toggle-nav]");
    const button2 = document.querySelector("[data-theme-toggle-mobile]");
    const button3 = document.querySelector("[data-theme-toggle-footer]");
    
    const localStorageTheme = localStorage.getItem("theme");
    const systemSettingDark = window.matchMedia("(prefers-color-scheme: dark)");
    const navSun = document.querySelector('[data-theme-toggle-nav] .sun');
    const navMoon = document.querySelector('[data-theme-toggle-nav] .moon');
    
    /*
    * 2. Work out the current site settings.
    */
    let currentThemeSetting = calculateSettingAsThemeString({ localStorageTheme, systemSettingDark });
    
    /* 
    * 3. Update the theme setting and button text according to current settings.
    */
    if (currentThemeSetting === "dark") {
        navSun.classList.toggle('visible');
        navMoon.classList.toggle('visible');
        button1.setAttribute("aria-label", "Dark mode is active");
    }
    updateButton({ buttonEl: button2, isDark: currentThemeSetting === "dark" });
    updateButton({ buttonEl: button3, isDark: currentThemeSetting === "dark" });
    updateThemeOnHtmlEl({ theme: currentThemeSetting });
    
    /*
    * 4. Add an event listener to toggle the theme.
    */
    function handleButtonClick(event) {
        const newTheme = currentThemeSetting === "dark" ? "light" : "dark";
        
        localStorage.setItem("theme", newTheme);
        updateButton({ buttonEl: button1, isDark: newTheme === "dark" });
        updateButton({ buttonEl: button2, isDark: newTheme === "dark" });
        updateButton({ buttonEl: button3, isDark: newTheme === "dark" });
        updateThemeOnHtmlEl({ theme: newTheme });
        
        currentThemeSetting = newTheme;
    }
    
    button1.addEventListener("click", handleButtonClick);
    button2.addEventListener("click", handleButtonClick);
    button3.addEventListener("click", handleButtonClick);
    
    /*
    * 5. Add an event listener to toggle checkbox on enter/return key press.
    */
    document.getElementById('dark-mode-toggle-mobile').addEventListener('keydown', function(event) {
        if (event.key === 'Enter' || event.key === ' ') {
            this.checked = !this.checked;
            event.preventDefault();
            button2.click();
        }
    });
    document.getElementById('dark-mode-toggle-footer').addEventListener('keydown', function(event) {
        if (event.key === 'Enter' || event.key === ' ') {
            this.checked = !this.checked;
            event.preventDefault();
            button3.click();
        }
    });

    </script>
    <?php
}

This comprehensive approach underscores our commitment to delivering a user-friendly and accessible website, where preferences are respected and easily managed.

Part 3: Dark mode styling with Sass

After finalizing the functionality of our dark mode toggle, we shifted our focus to styling, leveraging Sass to optimize our workflow. We introduced _darkmode.scss, a dedicated partial file for our dark mode styles, to ensure a clear separation and organization of our theme-based styling. This approach not only kept our codebase modular but also simplified the management of distinct visual themes, facilitating a cleaner and more structured stylesheet architecture.

Crucial to our styling strategy was the incorporation of Sass’ placeholder selectors, which allowed us to efficiently manage and reuse shared styles without redundancy. This technique minimizes the repetition of CSS code and keeps our stylesheets streamlined and maintainable. By employing placeholder selectors, we ensured consistent application of styles across various components, adhering to a DRY (Don’t Repeat Yourself) principle. This method has significantly contributed to a cohesive and scalable styling solution, enhancing the user experience across both light and dark modes.

Upon adding our sitewide dark mode styling, we prepared our accessible dark mode toggle button styling. Our Sass CSS has been included below as a reference. Please note that within our code we’re using Sass variables for handling our color palette. We are also using filters for adjusting our black SVG icons to the desired target hex color with the help of Barrett Sonntag’s CSS filter generator. Lastly, our mobile navigation menu and footer use the same switch-like toggle UI, whereas our main navigation menu uses a simple icon button to be more space efficient.


// Mobile navigation menu and footer dark mode toggle button styling.

.rf-darkmode-toggle {
    label {
        width: 120px;
        height: 45px;
        position: relative;
        display: block;
        background-color: $color-n1;
        border-radius: 200px;
        border: 1px solid lighten($color-n4,16%);
        cursor: pointer;
        transition: 0.3s;
        
        &:after {
            content: "";
            width: 35px;
            height: 35px;
            position: absolute;
            top: 4px;
            left: 6px;
            background: $link-color;
            border-radius: 180px;
            transition: 0.3s;
        }
        
        img {
            position: absolute;
            width: 24px;
            top: 10px;
            z-index: 100;
            user-select: none;
            
            &.sun {
                left: 12px;
                filter: invert(100%);
                transition: 0.3s;
            }
            
            &.moon {
                left: 83px;
                filter: invert(61%) sepia(0%) saturate(245%) hue-rotate(135deg) brightness(93%) contrast(92%);
                transition: 0.3s;
            }
        }
    }
    
    input {
        width: 120px;
        height: 45px;
        position: absolute;
        margin: 0;
        z-index: 0;
        appearance: none;
        
        &:checked + label {
            background: transparent;
            border: 1px solid $color-n3;
        }
        
        &:checked + label:after {
            left: 112px;
            transform: translateX(-100%);
            background: $link-color;
        }
        
        &:checked + label img.sun {
            filter: invert(61%) sepia(0%) saturate(245%) hue-rotate(135deg) brightness(93%) contrast(92%);
        }
        
        &:checked + label img.moon {
            filter: invert(100%);
        }
    }   
}

// Main navigation menu dark mode toggle button positioning.

#site-header .rf-darkmode-toggle {
    position: absolute;
    top: 29px;
    right: 0px;
    transition: all 0.3s ease-in-out;
    
    @include breakpoint-900-minus {
        display: none;
    }
    
    .container {
        width: 26px;
        height: 36px;
        box-sizing: border-box;
        padding: 0;
        background: none;
        border: none;
        display: flex;
        justify-content: center;
        align-items: center;
        position: relative;
        transition: all 0.3s ease-in-out;
        
        img {
            width: 100%;
            height: 100%;
            position: absolute;
            top: 0;
            left: 0;
            opacity: 0;
                    
            &.sun {width: 26px;}
            
            &.moon {
                filter: invert(100%);
                width: 20px;
                left: 3px;
            }
            
            &.sun, &.moon {transition: all 0.3s ease-in-out;}
            
            &.visible {opacity: 1;}
        }
    }
}

Best practices

Male Drawing Website Framework on Screen with Black Dry Erase Marker

While working on our dark mode styling, we adhered to Chrome for Developer’s dark mode best practices. We wanted to prioritize a comfortable dark mode experience that was not straining on the eyes. This involved some additional experimentation and testing to further refine. The following recommendations were addressed:

Avoid pure white

Pure white in dark mode is very vibrant and can quickly fatigue the eyes. We were careful to use light grays instead of white for our logo, navigation menu items, body copy and footer content.

Darken photographic images

The vast majority of images on our website have a 15% black overlay added to them. Those that don’t are set with an opacity of 85% to accomplish a similar result. We found this percentage to be adequate to reduce the brightness while still preserving the rich detail within the images themselves.

Invert vector graphics and icons

We inverted the SVG icons on our website by using the filter CSS function and setting relevant parameters. This is the declaration we decided upon: filter: invert(100%) brightness(85%);. Since our icons are colorful like our images, we found that it was helpful to also reduce their brightness by 15% for greater consistency across all of our graphics.

Make Google Maps dark mode friendly

While not mentioned within Chrome for Developer’s best practices guidelines, we’d also like to address how we approached our embedded Google Maps since many local businesses include them on their websites.

There are several pages on our website that have Google Maps embedded (one being our contact page). We duplicated our existing maps and applied dark mode friendly JavaScript styling from Snazzy Maps for our dark mode equivalents. Each version was then set to display based on the [data-theme="light"] or [data-theme="dark"] designation. The result is a seamless transition for google maps styling when dark mode is toggled.

Team credits

Determining how to approach our site’s accessible custom dark mode was a highly collaborative effort between our team members Max Elman, Vinícius Miazaki and myself. Max researched dark mode toggle reference articles, Vinícius implemented the JavaScript toggle, placement shortcodes and dark mode codebase, and I prepared the WCAG conformant dark mode styling, toggle UI components and Sass optimizations. Vinícius also graciously helped prepare our three-part approach. A very big thanks goes to them both.

That’s a wrap!

Dark Mode Toggle - Light and Dark Layouts Viewed by a Man on a Laptop

Razorfrog’s accessible dark mode toggle is a major accomplishment for our team and was a labor of love. We learned that developing an accessible custom dark mode is an exceptional challenge that requires research, trial and error, and — most importantly — patience.

Our initial approach was to respect users’ system settings and include dark mode styling alongside our light mode styling. But after viewing other websites’ examples, we decided that having a toggle would be even more ideal to provide our visitors with the most control over their user experience. In the end, we were able to accomplish our implementation goals and are very happy with the results.

We hope that this article proves to be helpful for others who are also working on creating accessible dark mode solutions for their websites. If you found our implementation helpful, please follow us and share on social media. Thanks for reading!

Scott is Razorfrog's Lead Designer. He is a passionate web designer, graphic designer, branding specialist, accessibility expert and content creator. View Scott's bio for more details.

Toggle light/dark mode

Back To Top