Dark Mode Setup Guide
Everything you need to integrate professional, flicker-free dark mode into your Tailwind CSS v4 project.
Inject CSS Variables
Paste your generated theme variables into your global CSS file. This establish the semantic tokens for both modes.
@theme {
/* BRAND */
--color-brand: var(--brand-default);
--color-brand-dark: var(--brand-dark);
--color-brand-light: var(--brand-light);
}
:root {
/* These variables are kept as aliases for the FOUC prevention script and logic */
--brand-default: #008294;
--brand-dark: #006b7d;
--brand-light: #50b2c5;
}
.dark {
--brand-default: #0a191b;
--brand-dark: #008294;
--brand-light: #83d4e4;
/* ... rest of your dark shades */
}Prevention Script
Add this minimal script to your <head> to prevent that annoying white flash when users refresh in dark mode.
<script>
if (localStorage.theme === 'dark' || (!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
</script>The Theme Toggle
Create a highly professional, hydration-safe React component to let users switch between modes. This prevents visual flickering and persists their choice to localStorage.
import { useEffect, useState } from 'react';
import { Sun, Moon } from 'lucide-react';
export const ThemeToggle = () => {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
const [mounted, setMounted] = useState(false);
// 1. Avoid hydration mismatches by syncing state on mount
useEffect(() => {
setMounted(true);
const isDark = document.documentElement.classList.contains('dark');
setTheme(isDark ? 'dark' : 'light');
}, []);
const toggleTheme = () => {
const nextTheme = theme === 'light' ? 'dark' : 'light';
const root = document.documentElement;
if (nextTheme === 'dark') {
root.classList.add('dark');
localStorage.theme = 'dark';
setTheme('dark');
} else {
root.classList.remove('dark');
localStorage.theme = 'light';
setTheme('light');
}
};
// Render a clean placeholder skeleton until hydration is complete
if (!mounted) {
return <div className="w-10 h-10 rounded-xl bg-bg-sunken animate-pulse" />;
}
return (
<button
onClick={toggleTheme}
aria-label="Toggle Theme"
className="p-2.5 rounded-xl border border-border-subtle bg-bg-surface text-text-body hover:scale-105 transition-all shadow-sm cursor-pointer"
>
{theme === 'dark' ? <Sun className="w-5 h-5" /> : <Moon className="w-5 h-5" />}
</button>
);
};Use Semantic Tokens
Stop using dark:bg-black. Instead, use the semantic variables you defined in Step 1. They handle the flip automatically.
// ✅ THE RIGHT WAY <div className="bg-bg-page text-text-body"> Theme-aware content </div> // ❌ THE OLD WAY <div className="bg-white text-black dark:bg-black dark:text-white"> Harder to maintain </div>
The "Flicker-Free" Secret
Most developers fail dark mode because they run the theme script *inside* their React component. This causes a white flash before React hydrates. By putting the minimal script in your head, the browser applies the dark theme before it even renders the first pixel.