Dark Mode Without the Flash
The classic dark mode flash problem: your page loads, briefly shows the wrong theme, then snaps to the correct one. I stuggled with this issue while writing this blog and eventaully figured out the solution.
Why the flash happens
React hydration is asynchronous. By the time your theme provider reads localStorage and sets the theme, the browser has already painted the default styles. The user sees a flash.
The simple fix: a blocking inline script
Add a small script directly to your <head> — before any stylesheets or React code runs:
<script
dangerouslySetInnerHTML={{
__html: `
try {
var t = localStorage.getItem('theme');
if (!t) t = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', t);
} catch(e) {}
`,
}}
/>
This runs synchronously, before paint, so the correct theme is applied before the browser renders anything visible.
CSS variables approach
Store your theme values in CSS custom properties on the :root:
:root {
--bg: #f5f5f0;
--text: #1a1a1a;
}
[data-theme="dark"] {
--bg: #0d0d0f;
--text: #e8e8e0;
}
Switching themes is then a single setAttribute call — no class toggling, no CSS-in-JS overhead.
The result
Zero flash. The browser reads the data-theme attribute set by the inline script, resolves the CSS variables, and paints correctly on the very first frame.