Dark Mode with Tailwind
Implement dark mode with Tailwind's dark variant, CSS custom properties, and user preference detection
When to Use
- Adding dark mode to an existing Tailwind application
- Supporting system preference detection and manual theme switching
- Using CSS variables so that one set of utilities works in both themes
- Persisting the user's theme choice across sessions
Instructions
- Choose a strategy:
class(manual toggle via.darkclass on<html>) ormedia(follows OS preference). Useclassfor manual control. - Define color tokens as CSS variables with light and dark values. Use Tailwind's
dark:prefix for overrides. - Prefer the CSS variable approach over per-element
dark:classes — it scales better with many components. - Detect the user's preference with
window.matchMedia('(prefers-color-scheme: dark)'). Store their choice in localStorage. - Apply the theme class before first paint to prevent flash of wrong theme (FOWT).
// tailwind.config.ts
const config: Config = {
darkMode: 'class', // Enable class-based dark mode
// ...
};
/* styles/globals.css */
@layer base {
:root {
--background: 255 255 255;
--foreground: 17 24 39;
--card: 249 250 251;
--primary: 59 130 246;
--muted: 107 114 128;
--border: 229 231 235;
}
.dark {
--background: 15 23 42;
--foreground: 248 250 252;
--card: 30 41 59;
--primary: 96 165 250;
--muted: 148 163 184;
--border: 51 65 85;
}
}
// hooks/use-theme.ts
import { useEffect, useState } from 'react';
type Theme = 'light' | 'dark' | 'system';
export function useTheme() {
const [theme, setThemeState] = useState<Theme>(() => {
if (typeof window === 'undefined') return 'system';
return (localStorage.getItem('theme') as Theme) ?? 'system';
});
useEffect(() => {
const root = document.documentElement;
function applyTheme(t: Theme) {
if (t === 'system') {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
root.classList.toggle('dark', isDark);
} else {
root.classList.toggle('dark', t === 'dark');
}
}
applyTheme(theme);
// Listen for OS preference changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handler = () => {
if (theme === 'system') applyTheme('system');
};
mediaQuery.addEventListener('change', handler);
return () => mediaQuery.removeEventListener('change', handler);
}, [theme]);
const setTheme = (t: Theme) => {
setThemeState(t);
localStorage.setItem('theme', t);
};
return { theme, setTheme };
}
// Prevent flash of wrong theme — add to <head> in your HTML/layout
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
var theme = localStorage.getItem('theme');
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
})();
`,
}}
/>
Details
CSS variable approach vs dark: prefix:
// Per-element dark: classes (verbose, repetitive)
<div className="bg-white dark:bg-slate-800 text-gray-900 dark:text-gray-100 border-gray-200 dark:border-gray-700">
// CSS variable approach (clean, DRY)
<div className="bg-background text-foreground border-border">
// Colors switch automatically via CSS variables
Approach comparison:
dark:prefix: Simple, no CSS variables needed. Gets verbose with many elements.- CSS variables: Define once, works everywhere. Best for design systems with many components.
- Both: Use CSS variables for common patterns,
dark:for one-off overrides.
Next.js with next-themes:
import { ThemeProvider } from 'next-themes';
function App({ children }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
);
}
Images and dark mode: Provide dark-mode variants for images, or use CSS filters:
<img className="dark:invert dark:brightness-90" src="/logo.svg" alt="Logo" />
// Or use next/image with dark mode source
Testing dark mode: In browser DevTools, toggle the dark class on <html>. Or use DevTools > Rendering > Emulate CSS media feature prefers-color-scheme: dark.
Source
https://tailwindcss.com/docs/dark-mode
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.