Dark mode looks simple until you hit the flash-of-wrong-theme problem — the page briefly renders in light mode before your JavaScript detects the user's dark mode preference and switches it, a visible flicker that's entirely avoidable with the right implementation order.
Dark mode implementation involves detecting the user's preferred color scheme (via the prefers-color-scheme media query or an explicit user toggle), applying corresponding styles (typically via CSS custom properties), and persisting the user's explicit choice across sessions — done correctly, without a visible flash of the wrong theme on page load.
Why Dark Mode Matters (and When to Deprioritize It)
Dark mode is now a widely expected feature for many applications — beyond aesthetic preference, it genuinely matters for accessibility (reduced eye strain in low-light conditions for many users) and battery life on OLED screens — and a correctly implemented dark mode (no flash, respects system preference, remembers explicit choice) is a meaningfully better experience than a naive implementation.
Deprioritize dark mode for internal tools or admin panels with a small, known user base where the effort isn't justified by actual user demand — but for any consumer-facing product, the expectation bar has risen enough that its absence is increasingly noticeable.
Getting Started with Dark Mode Implementation
CSS custom properties define theme values, switched by a data-theme attribute:
:root {
--bg-color: white;
--text-color: black;
}
[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: white;
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}
Avoiding flash of wrong theme requires setting the theme before the page renders — an inline script in the document head, before any content paints:
<script>
(function () {
const saved = localStorage.getItem("theme");
const theme = saved || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
document.documentElement.setAttribute("data-theme", theme);
})();
</script>
Core Dark Mode Implementation Concepts Every Developer Should Know
The flash-of-wrong-theme problem happens when theme detection runs after the page has already started rendering. The fix is running theme detection synchronously, inline, in the document head — before the browser has painted anything — rather than in a script that loads and executes after initial render, which is the common mistake that causes the visible flicker.
Three-state preference logic (light, dark, system) is more correct than a simple binary toggle. Respecting prefers-color-scheme by default (system), while allowing an explicit user override (light/dark) that persists, gives users the flexibility to either follow their OS setting automatically or lock in an explicit preference — a plain light/dark toggle alone loses the "follow system" option some users specifically want.
Persisting the explicit choice (not the system preference) in localStorage distinguishes "user explicitly chose dark" from "system happens to be dark." If you only ever read prefers-color-scheme without persisting an explicit override, you can't distinguish a user's deliberate choice from the system default, and their choice won't survive a system-level theme change.
Server-rendered applications need special handling to avoid a hydration mismatch. Since the server doesn't know the client's localStorage value or system preference at render time, a naive implementation can render one theme on the server and hydrate to a different one on the client — the inline head script approach sidesteps this by applying the theme attribute before React (or your framework) hydrates.
Common Dark Mode Implementation Mistakes and How to Fix Them
Mistake 1: detecting theme preference in a regular (non-inline, non-head) script that runs after initial paint, causing a visible flash of the wrong theme. Fix: use a synchronous inline script in the document head that runs before first paint.
Mistake 2: only supporting a binary light/dark toggle without a "system" option, forcing users who want to follow their OS setting to manually match it, and to remember to change it when their OS theme changes. Fix: default to prefers-color-scheme, with an explicit override that persists only when the user deliberately chooses one.
Mistake 3: not testing actual color contrast in dark mode, assuming inverted colors are automatically accessible — a dark mode isn't just light mode with inverted colors; contrast ratios need to be verified independently for the dark palette. Fix: check WCAG contrast ratios for the dark theme specifically, not just carry over the light theme's presumed-adequate contrast.
When Should You Default to System Preference Instead of a Fixed Default Theme?
Default to system preference (prefers-color-scheme) for any general consumer-facing application, since it respects the user's existing OS-level choice without requiring an explicit decision from them on first visit. Use a fixed default theme only when there's a specific product reason to override system preference (a brand-specific default, or a context where one theme is clearly more appropriate regardless of system setting).
Dark Mode Implementation in Production
Implement theme detection via a synchronous inline script in the document head to eliminate flash-of-wrong-theme, and persist explicit user choices distinctly from system-detected preference. Also verify dark mode's color contrast meets accessibility standards independently, rather than assuming an inverted palette is automatically fine.
If your current dark mode implementation has a visible flash on load, that's a well-understood, fixable problem — moving theme detection into an inline head script resolves it directly.