Implementing Dark Mode: CSS Variables, System Preference, and Persistence
Dark mode done right: system preference detection, manual toggle, and persistence — without a flash of white on load.
CSS Variables: The Foundation
:root {
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #1a1a1a;
--text-secondary: #6b7280;
--border: #e5e7eb;
--shadow: rgba(0, 0, 0, 0.1);
}
[data-theme='dark'] {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--border: #334155;
--shadow: rgba(0, 0, 0, 0.4);
}
/* Components use variables — not hardcoded colors */
body { background: var(--bg-primary); color: var(--text-primary); }
.card { background: var(--bg-secondary); border: 1px solid var(--border); }
System Preference Detection
/* Apply dark mode when system prefers it */
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
--bg-primary: #0f172a;
/* ... */
}
}
React Hook
function useTheme() {
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>(() => {
if (typeof window === 'undefined') return 'system';
return (localStorage.getItem('theme') as 'light' | 'dark' | 'system') ?? 'system';
});
useEffect(() => {
const root = document.documentElement;
if (theme === 'system') {
root.removeAttribute('data-theme');
} else {
root.setAttribute('data-theme', theme);
}
localStorage.setItem('theme', theme);
}, [theme]);
return { theme, setTheme };
}
Preventing Flash of Wrong Theme
<!-- In <head> — runs before body renders -->
<script>
(function() {
const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = stored === 'dark' || (!stored && prefersDark) ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', theme);
})();
</script>
Tailwind Dark Mode
// tailwind.config.js
module.exports = {
darkMode: 'class', // or 'media' for system-only
};
// Toggle dark class on <html>
function ThemeToggle() {
return (
<button
onClick={() => document.documentElement.classList.toggle('dark')}
className='bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
>
Toggle
</button>
);
}
Dark mode, theme persistence, and the full Tailwind + CSS variable setup are included in the AI SaaS Starter Kit — ships with both light and dark themes out of the box.
Top comments (0)