Implementing Zero-Flash System-Aware Dark Mode with Tailwind CSS v4 and React 19
Eliminating FOUT, synchronizing OS color schemes live, and managing CSS design tokens without layout shift.
The Anatomy of a Theme Flash (FOUT)
A common flaw in modern web applications is the flash of unstyled or mismatched theme (FOUT) upon initial page load. When a user with dark OS preferences visits a site, standard client-side hydration causes a bright white screen to flash for 100-300 milliseconds before React initializes and toggles a CSS class.
In this article, we examine how to achieve deterministic, zero-flash theme rendering in Next.js App Router while fully honoring system preferences and user manual overrides.
Phase 1: Dynamic Tokens with Tailwind CSS v4
With Tailwind CSS v4, theme tokens can map directly to standard CSS variables rather than hardcoded hex values. By declaring semantic color tokens inside `@theme` that point to `--color-*` variables, every utility class (like `bg-stone` or `text-ink`) responds dynamically to `.dark` or `.light` class changes.
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));
@theme {
--color-stone: var(--color-stone);
--color-ink: var(--color-ink);
--color-accent: var(--color-accent);
}
:root {
--color-stone: #E9E7E2;
--color-ink: #1A1917;
--color-accent: #24363B;
}
.dark {
--color-stone: #141311;
--color-ink: #EDEAE4;
--color-accent: #79B4B0;
}Dynamic theme token definitions in Tailwind CSS v4 enabling instant re-skinning.
Phase 2: Pre-Paint Execution with Synchronous Head Script
To guarantee zero flash, an inline JavaScript snippet must execute synchronously inside `<head>` prior to browser paint. The script checks `localStorage` first, and if unset, falls back to `window.matchMedia('(prefers-color-scheme: dark)')`.
Applying `suppressHydrationWarning` to `<html>` prevents React from complaining about the runtime class attributes.
(function() {
try {
var stored = localStorage.getItem('portfolio-theme');
var isDark = stored === 'dark' || (!stored && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) {
document.documentElement.classList.add('dark');
document.documentElement.classList.remove('light');
} else if (stored === 'light') {
document.documentElement.classList.add('light');
document.documentElement.classList.remove('dark');
}
} catch (e) {}
})();Synchronous pre-paint execution block preventing any visible theme flicker.
Phase 3: React 19 State Management via useSyncExternalStore
Rather than triggering cascading renders with `useEffect` and `useState`, modern React applications should subscribe to browser storage and media queries using `useSyncExternalStore`.
This approach guarantees tearing-free reads across tabs and synchronizes live when users switch their OS dark mode setting in real time.