Tailwind CSS in Production · Theming and Consistency
The White Flash in Dark Mode
Users who chose dark mode saw a bright white flash on every page load, because the dark class was added by JavaScript after the first paint.
Scenario
Example team: an app adds a theme toggle using class-based dark mode. A React effect reads the saved choice from localStorage and adds dark to <html> after the app mounts.
What you see in production
Every navigation in dark mode starts with a white page for a fraction of a second; on slow phones it lasts long enough to be painful at night. Users report "the flash".
The naive code
useEffect(() => {
if (localStorage.theme === "dark") document.documentElement.classList.add("dark");
}, []);
Reproduce it
The order of events decides what the user sees first:
def timeline(setup):
events = []
dark = False
for step in ["html parsed", "css loaded", "first paint", "js bundle runs", "react mounts", "effect runs"]:
if setup == "inline head script" and step == "html parsed":
dark = True # a tiny script in <head> runs before any paint
if setup == "useEffect" and step == "effect runs":
dark = True
if step == "first paint" or step == "effect runs":
events.append(f"{step}: {'dark' if dark else 'WHITE'}")
return events
for setup in ("useEffect", "inline head script"):
print(f"{setup:20}", " -> ".join(timeline(setup)))
Output
useEffect first paint: WHITE -> effect runs: dark inline head script first paint: dark -> effect runs: dark
Root cause
The browser paints as soon as HTML and CSS are ready. Code in your bundle, and especially in effects after hydration, runs after the first paint, so the page renders light first.
The fix
Set the class before the first paint with a tiny blocking script in <head>, and keep the toggle code for later changes:
<script>
document.documentElement.classList.toggle(
"dark",
localStorage.theme === "dark" ||
(!("theme" in localStorage) && matchMedia("(prefers-color-scheme: dark)").matches),
);
</script>
Also add <meta name="color-scheme" content="light dark"> (or color-scheme in CSS) so form controls and scrollbars match. Server-rendered apps can render the class from a cookie instead.
<style type="text/tailwindcss">
@custom-variant dark (&:where(.dark, .dark *));
</style>
<div class="grid sm:grid-cols-2">
<div class="bg-white p-6 text-gray-900 dark:bg-gray-950 dark:text-gray-100">Light page</div>
<div class="dark bg-white p-6 text-gray-900 dark:bg-gray-950 dark:text-gray-100">Dark page, same classes</div>
</div>
Trade-offs
- An inline script must be allowed by your Content-Security-Policy (use a nonce or hash).
- Following the system setting with the default media-query strategy needs no script at all, but gives users no choice.
Detect it in production
- Record a page load in dark mode with CPU throttling; any light frame is a flash.
- Check that the theme script is the first script in
<head>.
Key points
- Anything after first paint is too late for the theme class.
- Use an inline head script (or server rendering) to set it.
Practice questions
How do you prevent the flash of the light theme with class-based dark mode?
Answer
Set the dark class on <html> before first paint: a small inline script in <head> reads the saved preference (or the system setting) and toggles the class; or render it server-side from a cookie. Effects in the app bundle run too late.
Open in the app to mark lessons complete, take notes and track your progress.