Dark mode with no flash: what happens before first paint
You implement dark mode, you test it, it works. Then you reload and see a white flash before the dark theme appears. The classic FOUC.
The cause is not in the CSS. It is in when the JavaScript runs.
The race
The browser builds the page in order: parse HTML, fetch CSS, compute style, paint. If the script that decides the theme runs after that paint, the user sees the default theme first.
parse HTML → fetch CSS → first paint → DOMContentLoaded → your script
▲ │
flash happens here theme applied hereAny script with defer, async, or bound to DOMContentLoaded loses the
race by construction.
The fix
A synchronous inline script in <head>, before any stylesheet that
depends on the theme:
<script>
(function () {
var stored = localStorage.getItem("color-theme");
var theme = stored === "light" || stored === "dark"
? stored
: (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
document.documentElement.classList.add(theme);
document.documentElement.style.colorScheme = theme;
})();
</script>It blocks the parser for well under a millisecond, and the class is already on
<html> when the first pixel appears.
The two details almost everyone forgets
color-scheme. Without it the browser still paints scrollbars, form
controls and the default canvas in light. It is one line, and it is the
difference between “dark” and actually dark.
localStorage can throw. In some browsers’ private mode, accessing it
raises. Wrap it in try/catch — the site has to load even when the preference
cannot be read.
Three states, not two: light, dark and system. System is the correct default, and the only one that respects someone who changed their OS preference after visiting your site.
Verifying it
The honest test is with CPU throttling on, 6x slowdown, cache disabled. The flash you cannot see on your desktop shows up immediately on a mid-range phone.