DEV3 Technologies home

Dark and light mode toggle without flash

A theme-level colour mode switch that respects the visitor's system setting, remembers a choice and avoids a flash of the wrong theme.

WordPressThemingdark-modetoggleaccessibilitycss-variables

Live preview

Code

<?php
/**
 * Dark / light mode toggle.
 *
 * @package Acme_Theme
 */

defined( 'ABSPATH' ) || exit;

// Runs before paint so the saved or system mode is applied with no flash.
function acme_mode_bootstrap(): void {
	echo "<script>(function(){var m;try{m=localStorage.getItem('acme-mode');}catch(e){}"
		. "if(!m){m=matchMedia('(prefers-color-scheme: dark)').matches?'dark':'light';}"
		. "document.documentElement.setAttribute('data-mode',m);})();</script>\n";
}
add_action( 'wp_head', 'acme_mode_bootstrap', 1 );

function acme_mode_assets(): void {
	$dir = get_template_directory_uri() . '/assets/';
	wp_enqueue_style( 'acme-mode', $dir . 'dark-mode.css', array(), '1.0.0' );
	wp_enqueue_script(
		'acme-mode',
		$dir . 'dark-mode.js',
		array(),
		'1.0.0',
		array( 'in_footer' => true, 'strategy' => 'defer' )
	);
}
add_action( 'wp_enqueue_scripts', 'acme_mode_assets' );

// Use [acme_mode_toggle] in a header template part or block.
function acme_mode_toggle_shortcode(): string {
	return sprintf(
		'<button type="button" class="acme-mode-toggle" aria-pressed="false" data-label-dark="%1$s" data-label-light="%2$s">%3$s</button>',
		esc_attr__( 'Switch to dark mode', 'acme-theme' ),
		esc_attr__( 'Switch to light mode', 'acme-theme' ),
		esc_html__( 'Toggle colour mode', 'acme-theme' )
	);
}
add_shortcode( 'acme_mode_toggle', 'acme_mode_toggle_shortcode' );

How it works

What it solves

Visitors expect a dark option, but most implementations flash white on load or ignore the operating system setting. This pattern picks the right mode before the first paint and keeps a manual choice between visits.

How it works

A tiny inline script in wp_head runs at priority 1 and sets data-mode on the root element from localStorage, falling back to prefers-color-scheme. All colours are CSS custom properties redefined under [data-mode="dark"], so no class juggling is needed. The deferred script wires every .acme-mode-toggle button, updates aria-pressed and the accessible label, and stores the choice in a try/catch because storage can be blocked.

A shortcode prints the button wherever you need it.

Adapting it

Add more variables to the two blocks of custom properties and use them across your stylesheet. To follow the system setting live, listen to matchMedia(...).addEventListener('change', ...) when no manual choice exists. If you use theme.json, map its palette to these variables.

Pitfalls

  • The inline script must stay in the head, otherwise the flash returns.
  • Caching plugins are fine because the mode is decided in the browser.
  • Do not store anything in a cookie just for this; it would break full-page caching.
  • Check contrast in both modes, not only the light one.

How to use it

Copy the three files into inc/ and assets/, then require inc/dark-mode.php from functions.php.

Place [acme_mode_toggle] in your header, or call echo do_shortcode( '[acme_mode_toggle]' ); in a template.

Style your own components with var(--acme-bg) and var(--acme-text).

Licence: GPL-2.0-or-later. This snippet comes from WordPress code and is GPL-2.0-or-later.

Related snippets