“Add dark mode” sounds like one feature request, but it’s really three different technical decisions wearing the same name. A site that just wants to respect a visitor’s OS setting needs nothing but CSS. A site that wants a visible sun/moon toggle a reader can click needs JavaScript and a place to remember the choice. A site built on a classic (non-block) theme has a real constraint the block-theme world doesn’t: no theme.json to lean on, full stop. Picking the wrong one of these for what you actually want is the most common way this feature goes sideways.
The three real approaches
Before touching code, decide which of these you’re actually building — they solve different problems and involve different amounts of work.
1. Automatic, OS-matched, no toggle (pure CSS)
This is the prefers-color-scheme media query. It reads the visitor’s own operating-system or browser preference and switches your styles to match — nothing to click, nothing to store, no JavaScript at all:
@media (prefers-color-scheme: dark) {
body {
background-color: #121212;
color: #ffffff;
}
a {
color: #8ab4f8;
}
}
Add this to a child theme’s style.css (see the guide on creating a WordPress child theme before editing theme files directly) or, for a quick test, through Appearance > Customize > Additional CSS. This is the fastest and most reliable option: the browser handles everything natively, there’s no flash of the wrong theme, and no extra HTTP request. The real limitation is that there’s no toggle — a visitor whose OS is set to light mode can’t preview your dark version, and vice versa, without changing their system setting.
2. A visible toggle, via plugin
If you want an actual switch a reader can click — independent of their OS setting — a plugin is the fastest path, and it’s the only route worth taking if you don’t want to write and maintain your own JavaScript. Two names come up consistently in 2026: WP Dark Mode (by WPPOOL) is the established option, with 20,000+ active installs; its free tier includes a floating toggle, OS auto-detection, a Gutenberg block, an Elementor widget, admin-dashboard dark mode, two color presets, and image-brightness adjustment. Darkify is the newer challenger, and it’s worth a specific look because its free tier ships several features WP Dark Mode reserves for its paid “Ultimate” plan: time-based scheduling, eight built-in color presets, editor-specific dark mode, and per-element control. If your budget is genuinely $0 and you want more than a bare toggle, Darkify’s free plan is the better starting point; if you’d rather use the more established, better-documented plugin and are fine paying for scheduling or extra presets later, WP Dark Mode is the safer long-term bet.
3. A visible toggle, hand-built
Custom-coding your own toggle makes sense when a plugin’s styling doesn’t match your theme closely enough, or when you want zero extra plugin overhead. The mechanics are the same regardless of theme: a small script reads a stored preference (or falls back to the OS setting) and adds a class to the page, and your CSS keys off that class instead of (or alongside) prefers-color-scheme.
The part people get wrong here isn’t the toggle itself — it’s when the preference gets applied. If you check localStorage and add the dark-mode class after the page has already started rendering, visitors see a flash of the wrong theme before it corrects itself (FOUC — flash of unstyled/incorrect content). The fix is to run that check inline, in the page’s <head>, before the browser paints anything:
<script>
(function() {
var stored = localStorage.getItem('theme');
var prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (stored === 'dark' || (!stored && prefersDark)) {
document.documentElement.classList.add('dark');
}
})();
</script>
Placing this in the theme’s header.php (or via a “scripts in head” hook if you’d rather not edit template files directly) means the correct theme is already active by the time the page is visible — no flicker, no correction after the fact.
Classic themes vs. block themes: a real difference, not a technicality
This matters more than most dark-mode guides mention. Block themes (the ones built around theme.json) can define style variations and color palettes in a structured, theme-native way. Classic themes — GeneratePress, Astra, and most of the still-widely-used theme ecosystem — don’t read theme.json at all; adding one to a classic child theme is silently ignored, on both the frontend and the editor. If you’re on a classic theme, there’s no shortcut around plugin or custom CSS/JS — that’s the only route available, not a fallback for people who didn’t set things up “properly.”
Practical tips
- Dim your images, don’t just leave them. A product photo or screenshot shot on a white background looks washed out and harsh against a dark page. A simple
filter: brightness(0.85)on images inside your dark-mode CSS block softens this without needing separate dark-mode image assets. - Check contrast again, separately. A palette that passes accessibility contrast checks in light mode doesn’t automatically pass in dark mode — light text on a near-black background can undershoot the same WCAG ratio a dark-on-light pairing clears easily. Re-run your color and font choices through a contrast checker for the dark palette specifically, not just the light one.
- Write your dark values into your style guide as a second column, not an afterthought. If your light-mode palette lives in one place, the dark-mode equivalents belong right next to them — it’s the only way a future edit doesn’t quietly break one mode while fixing the other.
- Custom icons need their own check. A dark navy icon that reads clearly on a white background can disappear entirely against a near-black one. If you’re using the techniques from the guide to using icons in WordPress design, confirm each one still has real contrast in both modes before calling dark mode done.
Common mistakes
- Testing only in the browser’s dev-tools emulator. Chrome and Firefox let you force
prefers-color-schemewithout changing your OS setting, which is convenient — but it’s easy to ship something that only accounts for that one code path and never actually test the localStorage/no-preference-set case a real first-time visitor hits. - Hardcoding colors instead of using CSS custom properties. If your light-mode CSS uses literal hex values scattered across dozens of rules, a dark-mode override has to fight every one of them individually. Defining colors as CSS variables (
--bg-color,--text-color) once, then overriding just the variables inside the dark-mode media query or class, is dramatically less error-prone as a site grows. - Skipping the FOUC fix because it “mostly works.” A flash that only shows up for visitors with no stored preference yet — meaning every new visitor — is still a real, visible bug, even if it never shows up in your own testing because your own browser already has a preference saved.
Which one should you actually build?
If you just want to be a good citizen for visitors whose OS is already in dark mode and don’t care about a visible switch: the pure-CSS prefers-color-scheme approach, and nothing else. If you want a real toggle and don’t want to maintain custom code: Darkify’s free tier if you want more out of $0, WP Dark Mode if you’d rather have the more established, better-supported option. If you want a toggle that matches your theme’s styling exactly and are comfortable with a small amount of custom code: hand-build it, and don’t skip the inline head-script step — that’s the one part of a DIY toggle that actually requires getting the sequencing right.

Etienne Basson works with website systems, SEO-driven site architecture, and technical implementation. He writes practical guides on building, structuring, and optimizing websites for long-term growth.