A dark-mode toggle is a small feature with an outsized expectation problem: visitors who use dark mode across every app they touch notice immediately when a site doesn’t offer it, or offers a broken version that only half-applies. Here’s what a proper implementation actually needs to cover, and the block-native and CSS-only paths to get there.

What a Proper Dark Mode Actually Requires
- A full alternate color set (backgrounds, text, borders, links) defined for dark mode — not just an inverted filter slapped over the light version, which usually breaks images and brand colors.
- A visible toggle the visitor controls themselves, plus respect for their OS-level
prefers-color-schemesetting as the default before they’ve made an explicit choice. - The choice persisted (typically via a cookie or
localStorage) so it survives a page reload or a new page in the same session. - Every themed element covered — a dark toggle that misses your footer widgets, a specific block type, or embedded content looks more broken than no dark mode at all.
Block-Native: If Your Theme or Blocks Plugin Ships It
Some block themes and blocks plugins expose dark mode as a setting, with a preset toggle position, a preconfigured dark palette derived from your existing brand colors, and the persistence logic already handled. This is the path with the least manual work, since it’s built to cover the site’s existing blocks automatically rather than requiring you to write CSS for every block type yourself.

CSS-Only, If You’re Building It Yourself
Define your dark palette as CSS custom properties under a [data-theme='dark'] (or similar) attribute selector on the root element, mirror every color variable your light theme already uses, then add a small script that toggles the attribute and stores the preference:
:root {
--color-bg: #ffffff;
--color-text: #1a1a1a;
--color-border: #e2e2e2;
}
:root[data-theme='dark'] {
--color-bg: #12121a;
--color-text: #e8e8ec;
--color-border: #2e2e38;
}
body {
background: var(--color-bg);
color: var(--color-text);
}
// Toggle script, roughly:
const root = document.documentElement;
const saved = localStorage.getItem('theme');
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
root.setAttribute('data-theme', saved || (systemDark ? 'dark' : 'light'));
toggleButton.addEventListener('click', () => {
const next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', next);
localStorage.setItem('theme', next);
});
Layer in @media (prefers-color-scheme: dark) as the fallback default for visitors who haven’t made an explicit choice yet (the script above already checks this before falling back), guarded so an explicit toggle always overrides the OS preference in both directions via the stored localStorage value taking priority.
The color-scheme Meta Tag: The Fix Most Implementations Skip
Adding <meta name="color-scheme" content="dark light"> to your <head> tells the browser itself — not just your CSS — which schemes the page supports. This matters because it takes effect before any stylesheet has parsed, which is why it eliminates the white flash a dark-mode visitor otherwise sees for a split second while the page loads and your custom-property CSS kicks in. It also hands scrollbars, native form controls, and the default page background to the browser to theme correctly on its own, which is the piece most hand-rolled dark modes get wrong.
A Faster Path: the light-dark() CSS Function
Chrome, Safari, and Firefox all shipped the CSS light-dark() function in 2024, making it baseline-available across major browsers. Paired with the color-scheme property, it lets you set both values for a custom property in one line instead of duplicating every variable inside a separate dark-mode selector block:
:root {
color-scheme: light dark;
--color-bg: light-dark(#ffffff, #12121a);
--color-text: light-dark(#1a1a1a, #e8e8ec);
--color-border: light-dark(#e2e2e2, #2e2e38);
}
body {
background: var(--color-bg);
color: var(--color-text);
}
This doesn’t replace the toggle-and-localStorage pattern above — you still need the attribute-selector approach if visitors should be able to override their OS setting — but for sites that only need to follow prefers-color-scheme without an explicit toggle, it halves the CSS you have to maintain.
Also Read: Nexter Theme Global Styles: How to Set Up Brand Colors, Fonts and Spacing Sitewide — your brand colors need a defined dark-mode counterpart for each one; this is where those tokens live.
Also Read: How to Add a Sticky Header in WordPress (CSS and Block-Native Methods) — another CSS-custom-property-driven feature, if you’re implementing both by hand.
The Elements Sites Most Often Forget
- Embedded content — an embedded video player, map, or third-party widget rarely inherits your CSS variables and needs its own dark-mode handling, or an explicit exception.
- Form fields and buttons — browser-default form styling doesn’t automatically follow your custom properties unless you set
color-scheme: light dark, which hands date pickers, selects, and file inputs to the browser to theme correctly, or you explicitly restyle inputs, selects, and buttons for the dark palette yourself. - Images with transparent backgrounds — a logo or icon designed for a light background can become invisible or look wrong against a dark one; a dark-mode-specific asset swap is sometimes the only real fix.
A Pre-Launch Testing Checklist
Before shipping a dark-mode toggle, walk through every page template — not just the homepage — with the toggle on: check archive pages, single posts, search results, 404s, and any custom template. Confirm contrast holds up (text against its background should still be comfortably readable, not just technically visible), check that focus outlines and hover states are still distinguishable, and reload mid-session to confirm the stored preference survives navigation rather than resetting to the OS default on every new page.
Contrast Isn’t Optional, Even in Dark Mode
It’s tempting to treat a dark palette as a purely aesthetic choice, but the same accessibility baseline applies as it does in light mode: body text needs a contrast ratio of at least 4.5:1 against its background to meet WCAG AA, and large headline text needs at least 3:1. Pure white text (#ffffff) on pure black (#000000) technically passes but tends to produce a harsh halation effect for many readers — most well-built dark palettes use an off-white (something like #e8e8ec) against a dark gray-blue rather than true black, which is both easier to read for long stretches and closer to what OS-level dark themes do by default. Run your final palette through a contrast checker for both the primary text/background pairing and any secondary text (captions, meta info, muted labels), since those are the pairings that most often slip below the threshold once a palette gets inverted.
Performance: What a Toggle Script Should (and Shouldn’t) Cost
The toggle script itself is a handful of lines and negligible on its own, but two mistakes turn a dark-mode feature into a Core Web Vitals problem. First, applying the data-theme attribute after the page has already painted causes a visible flash of the wrong theme (sometimes called FOUC for dark mode); the fix is to inline that small attribute-setting script in the <head>, before your main stylesheet and before the rest of the page renders, so the correct theme is set on the very first paint. Second, avoid loading a second, separate dark-mode stylesheet as an extra network request — keeping both palettes in the same stylesheet as CSS custom properties (or using light-dark()) means the browser only ever fetches one file, and switching themes is just a class or attribute change with no additional requests or layout recalculation beyond repainting colors.
Where to Put the Toggle, and Common Mistakes That Undermine It
Header navigation (usually near the primary menu or a utility bar) is the most common and most discoverable placement, since it’s visible on every page without scrolling; a fixed corner button is the second-most-common pattern and works well on sites without a persistent header. Wherever it lands, keep it in the same spot across templates — a toggle that moves between the header on posts and a footer on pages forces visitors to hunt for it twice.
- Forgetting the icon itself needs both states — a sun/moon icon swap (or a single icon that visually communicates “tap to switch”) should flip along with the theme; a static icon that always shows “moon” regardless of the current mode reads as broken, even when the underlying toggle works correctly.
- Hardcoding colors in inline styles — any color set with an inline
styleattribute, or with a value baked into markup rather than pulled from a CSS custom property, won’t respond to the theme switch at all and will visually clash against the rest of the now-dark page. - Skipping the <html> or <body> background — if only inner containers get themed and the root element keeps its default light background, visitors see a flash of light-colored margin around the edges of an otherwise-dark page, especially on window resize or during scroll overscroll on mobile.
- Third-party embeds with their own chrome — comment widgets, chat plugins, and cookie-consent banners are frequently built by separate teams with their own fixed color values; audit these separately, since your site’s dark-mode CSS has no reach into an iframe’s internal styling.
FAQ
Should dark mode be on by default, or opt-in?
Default to the visitor’s OS-level preference (prefers-color-scheme) when they haven’t made an explicit choice, then let an explicit toggle override that in either direction.
Why do my images look wrong in dark mode?
This usually happens with a CSS-filter-based invert-everything approach rather than a proper dedicated dark palette; images and brand-colored elements need to be excluded from any blanket inversion filter, and transparent-background logos may need a dedicated dark-mode asset.
Does dark mode affect SEO?
Not directly. It’s a rendering/UX feature, not a ranking factor; the main risk is a broken implementation hurting readability or Core Web Vitals if it adds heavy layout-shifting scripts.
Why don’t my form fields switch to dark mode with everything else?
Browser-default form styling doesn’t automatically inherit custom CSS properties; setting color-scheme: light dark hands native controls like date pickers and selects to the browser to theme, and anything left over needs its own explicit dark-mode style rules.
What’s the difference between the color-scheme meta tag and prefers-color-scheme?
prefers-color-scheme is a media query you use in CSS to detect the visitor’s OS setting. The color-scheme meta tag (or CSS property) is a separate declaration telling the browser which schemes your page actually supports, so it can theme native UI like scrollbars and form controls and avoid a white flash before your stylesheet loads. Most complete implementations use both.
Conclusion
A proper dark mode is a full second color palette maintained alongside your light one, not a CSS filter bolted onto it — that distinction is what separates an implementation visitors barely notice from one that visibly breaks images and brand colors. Whichever path you take, pair your custom properties with the color-scheme meta tag so native browser UI themes correctly too, and test every themed element, including forms and embedded content, not just the obvious ones.
Suggested Reading
- Nexter Theme Global Styles: How to Set Up Brand Colors, Fonts and Spacing Sitewide
- How to Add a Sticky Header in WordPress (CSS and Block-Native Methods)
- Web Design Trends 2026 (and How to Build Every One in Gutenberg)
Stay updated with Helpful WordPress Tips, Insider Insights, and Exclusive Updates – Subscribe now to keep up with Everything Happening on WordPress!










