Dark mode interface design in WordPress theme development with CSS variables

Dark Mode in WordPress Themes: The Implementation Most Sites Get Wrong

Most dark mode implementations are broken by design, not by accident. Here is the CSS architecture and detection strategy that actually holds up in production.

Most WordPress dark mode implementations are broken. Not visually broken at first glance, but structurally broken in a way that guarantees problems as soon as the site grows past its initial setup. A single CSS class toggled on the body element, with hard-coded color values scattered across hundreds of rules, is not a dark mode system. It is a maintenance debt that compounds with every plugin added and every design change made.

Why Most Implementations Fail

The typical pattern developers reach for looks like this: add a .dark-mode class to <body>, then write override rules for each component:

.dark-mode { background: #1a1a1a; }
.dark-mode h1, .dark-mode h2 { color: #ffffff; }
.dark-mode .card { background: #2a2a2a; border-color: #444; }
.dark-mode .sidebar { background: #1f1f1f; }

This becomes unmanageable at scale. Every component needs its own override block. Plugin-injected markup gets missed. Third-party widgets stay light. And because every color value is hard-coded twice -- once for light, once for dark -- a single brand color change requires edits across dozens of rules in both modes.

The root cause is never the dark mode feature itself. It is the absence of a centralized color system. Fix the architecture and dark mode becomes a side effect, not a separate feature.

The Foundation: CSS Custom Properties

The correct foundation for any dark mode system is CSS custom properties, also called CSS variables. Instead of hard-coding color values, you define named tokens at the root level:

:root {
  --color-bg: #ffffff;
  --color-bg-secondary: #f5f5f5;
  --color-text: #1a1a1a;
  --color-text-muted: #6b7280;
  --color-border: #e5e7eb;
  --color-primary: #ff6b35;
  --color-surface: #ffffff;
}

Every element in your theme references these tokens, never raw hex values:

body { background: var(--color-bg); color: var(--color-text); }
.card { background: var(--color-surface); border-color: var(--color-border); }
h2 { color: var(--color-text); }

When dark mode activates, you override the tokens in one place. Every element updates automatically because they all reference the same variables:

[data-theme="dark"] {
  --color-bg: #0a0a0a;
  --color-bg-secondary: #141414;
  --color-text: #e5e5e5;
  --color-text-muted: #9ca3af;
  --color-border: #2d2d2d;
  --color-surface: #1a1a1a;
}

The MDN documentation on CSS custom properties covers the full specification, including cascade behavior and inheritance rules that affect how tokens propagate through nested components. CSS custom properties are supported in all modern browsers. If your theme still needs Internet Explorer 11 support, PostCSS transforms can compile them down, but that is an increasingly rare constraint in 2026.

This token-based approach is exactly how design systems like Tailwind CSS and Material Design work under the hood. The same principle applies whether you are building a WordPress theme or a React application.

Respecting System Preferences Automatically

Modern operating systems expose the user's color scheme preference to browsers through the prefers-color-scheme media query. A properly built WordPress theme should respect this setting by default, before any user interaction with a toggle:

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #0a0a0a;
    --color-bg-secondary: #141414;
    --color-text: #e5e5e5;
    --color-text-muted: #9ca3af;
    --color-border: #2d2d2d;
    --color-surface: #1a1a1a;
  }
}

With this in place, users who have set their OS or browser to dark mode get a dark interface automatically. No JavaScript required. No toggle interaction. The experience is immediate and consistent with every other app on their device.

Google's web.dev guide on prefers-color-scheme documents the detection logic and browser support matrix. The media query has had reliable support since 2019 across Chrome, Firefox, Safari, and Edge.

This matters for WordPress developers because it directly affects Core Web Vitals. A JavaScript-only dark mode implementation causes what the performance community calls a Flash of Incorrect Theme: the page renders in light mode first, then flickers to dark once the script fires. CSS-based detection eliminates this entirely because styles apply before any paint. If performance is already on your radar, the techniques in our WordPress performance optimization guide work alongside CSS-based dark mode without conflict.

Adding a User Toggle

Automatic detection handles the default case, but users want control. Someone might prefer dark mode at the OS level but want light mode for a specific site. The user toggle solves this by storing an explicit preference that overrides system detection.

The implementation sets a data-theme attribute on the <html> element, toggled via JavaScript and persisted in localStorage:

const toggle = document.querySelector('.theme-toggle');
const stored = localStorage.getItem('theme');

if (stored) {
  document.documentElement.setAttribute('data-theme', stored);
}

toggle.addEventListener('click', () => {
  const current = document.documentElement.getAttribute('data-theme');
  const next = current === 'dark' ? 'light' : 'dark';
  document.documentElement.setAttribute('data-theme', next);
  localStorage.setItem('theme', next);
});

Your CSS then prioritizes the explicit data-theme attribute over the media query. Put the media query first as the baseline, then the attribute overrides. The more specific selector wins:

/* Baseline: system preference */
@media (prefers-color-scheme: dark) {
  :root { /* dark token values */ }
}

/* Explicit overrides: user's stored choice wins */
[data-theme="dark"] { /* dark token values */ }
[data-theme="light"] { /* light token values, resets dark media query */ }

One critical detail: apply the stored theme before the first paint. Add a small inline script inside <head> to read localStorage and set the attribute before any CSS is applied:

<script>
  const t = localStorage.getItem('theme');
  if (t) document.documentElement.setAttribute('data-theme', t);
</script>

This is the one case where a render-blocking script is intentional. The script is under 200 bytes and eliminates a visible flash of incorrect theme on page load. Skip it and users with a stored dark preference will see a white flash on every reload. For WordPress themes, add this to header.php or the equivalent block theme template part that renders the document head.

Accessibility Requirements for Dark Mode

Switching colors does not automatically mean your dark mode meets accessibility standards. The Web Content Accessibility Guidelines (WCAG) 2.1, maintained by the World Wide Web Consortium, require a minimum 4.5:1 contrast ratio for normal text and 3:1 for large text and UI components.

Dark backgrounds with insufficiently light gray text fail this threshold often. Pure white text (#ffffff) on a very dark background (#0a0a0a) achieves a contrast ratio of 21:1, which meets guidelines but can cause eye strain over long reading sessions for some users. A softer approach, like #e5e5e5 on #0a0a0a, delivers a ratio around 17:1 and is more comfortable for extended use without failing any standard.

The US federal government's Section 508 accessibility requirements mandate WCAG 2.0 Level AA compliance for federal websites, including the 4.5:1 contrast threshold. While this applies specifically to federal agencies, it is the standard most enterprise clients expect and what Google uses as a benchmark in accessibility audits.

Light-on-dark color schemes have a documented history across interface design, with research covering both readability benefits and the specific conditions under which dark interfaces outperform light ones. The summary: dark mode is advantageous in low-light environments and for users with certain visual sensitivities, but it is not universally superior.

Use the WebAIM contrast checker to verify every text and background color combination in both your light and dark themes. Light mode compliance does not guarantee dark mode compliance. They require two separate audits.

Common Mistakes to Avoid

Mistake Why It Fails Fix
Hard-coded hex values in CSS Every component needs separate dark overrides Switch to CSS custom properties throughout
JavaScript-only detection Flash of incorrect theme on load Add prefers-color-scheme as CSS baseline
Forgetting plugin-injected markup Forms, galleries, sliders stay light-colored Test with all active plugins, use token-based colors
No explicit light override OS dark mode users cannot switch to light Add [data-theme="light"] with light token values
Missing inline head script Stored preference loads after first paint Add localStorage check before </head>
Skipping accessibility audit on dark palette Contrast ratios differ between themes Run WebAIM checks on both light and dark independently

Testing Your Implementation

Before shipping, verify these scenarios manually in a real browser:

  1. OS dark mode on, no stored preference: site should render dark
  2. OS light mode on, no stored preference: site should render light
  3. OS dark mode on, user toggled to light: site should render light
  4. OS light mode on, user toggled to dark: site should render dark
  5. Hard reload with stored preference: no visible flash of incorrect theme
  6. Plugin components (forms, galleries, sliders): check each in both modes
  7. Third-party embeds: flag YouTube iframes and social widgets as exceptions since you cannot control their internal styling

Chrome DevTools makes this straightforward without changing OS settings. Open DevTools, navigate to the Rendering tab (available via the three-dot menu under More Tools), and use the "Emulate CSS media feature prefers-color-scheme" dropdown to switch between light and dark detection without leaving your browser.

Start at the Architecture Level

Retrofitting dark mode onto an existing theme with hard-coded colors is expensive. If you are evaluating themes for a new project, check whether they use CSS custom properties as their color system. That architecture decision signals the developer built for extensibility. Our theme customization guide covers how to inspect and extend token systems in well-built themes, including child theme approaches that preserve your changes across updates.

For teams working with the newer block theme format, the theme.json implementation guide in our tutorials section covers how global style tokens in theme.json interact with CSS custom properties and where dark mode tokens fit within that system.

Dark mode done right is invisible to the user. They get the interface that matches their preference, with zero flash, full plugin compatibility, and proper contrast. The investment in a token-based color system pays back every time a design change, plugin update, or new feature would otherwise have required duplicate edits across two color modes.