Dark Mode for React and Vue SPAs That Use Hydration
Patrick Bushe
October 1, 2025 · 5 min read
Dark mode in a server-rendered React or Vue app has two classic bugs: a flash of the light theme before the dark one kicks in, and hydration warnings because the server rendered one theme and the browser wanted the other. Both come from the same thing: the server doesn't know the user's preference.
Why the flash happens
The server sends HTML with a default theme. The browser paints it. Then your JavaScript loads, reads localStorage or prefers-color-scheme, and switches to dark. That gap, often a few hundred milliseconds, is the white flash. Hydration adds a second problem: if the client renders different markup from the server, React and Vue warn about a mismatch or re-render the tree.
Fix 1: set the theme before first paint
Put a tiny blocking script in the <head>, before your stylesheets, that sets a class or data attribute on <html>:
<script>
try {
var t = localStorage.getItem('theme');
if (!t) t = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.dataset.theme = t;
} catch (e) {}
</script>
Style everything off that attribute. Because the script runs before paint, the first frame is already correct.
Fix 2: keep themed markup out of hydration
- Theme with CSS, not render output. If components render the same markup in both themes and only CSS changes, server and client always match.
- React: when
<html>attributes are set by the head script, addsuppressHydrationWarningto that element. For a toggle icon that depends on the theme, render it only after mount. - Vue and Nuxt: Nuxt's color mode module injects the head script for you. In plain Vue SSR, use the same inline script and read the attribute in
onMounted.
Fix 3: let the server know
Store the user's choice in a cookie as well as localStorage. The server can then render the right theme from the start, which removes the flash even with JavaScript off. Add <meta name="color-scheme" content="light dark"> and the CSS color-scheme property so scrollbars and form controls match too.
Test it properly
Throttle the network and CPU in DevTools, hard-reload in dark mode, and watch the first frames. If you see white, something is setting the theme after paint.