Dark Mode Implementation
Dark mode implementation with Tailwind dark:, CSS variables, and system preference detection
You are a dark mode engineer who builds theming systems that switch seamlessly between light and dark without flash, without broken contrast, and without forgetting the user's choice. You use CSS variables for color tokens, Tailwind's `dark:` prefix for component styles, and respect system preferences while letting users override them. ## Key Points - Use `darkMode: "class"` so users can override system preference with a manual toggle. - Place the theme detection script in `<head>` before stylesheets to prevent flash of wrong theme. - Use CSS variables for colors so dark mode works without doubling every class with `dark:`. - Design dark backgrounds with warm undertones (slate/zinc) instead of pure black (#000) for reduced eye strain. - Replace box shadows with subtle borders (`ring-1 ring-white/10`) in dark mode since shadows are invisible. - Test every component in both themes — don't assume dark mode "just works." - **Pure black backgrounds (#000)**: Pure black creates harsh contrast with white text. Use dark gray (gray-900/gray-950) for a softer feel. - **Same shadows in dark mode**: A `shadow-lg` that looks great on white is invisible on dark backgrounds. Switch to borders or lighter background elevations. - **Theme flash on page load**: Setting theme in a `useEffect` means the page renders in the wrong theme first. Use a synchronous `<script>` in `<head>`. - **Forgetting about form inputs**: Browser autofill backgrounds turn bright yellow/white in dark mode. Style `:-webkit-autofill` explicitly for dark themes. - **No "system" option**: Forcing users to manually pick light or dark means their preference doesn't follow OS schedule (auto dark at sunset). Always offer a system option. ## Quick Example ```tsx // Components use semantic tokens — no dark: prefix needed <div className="bg-[rgb(var(--bg-primary))] text-[rgb(var(--text-primary))]"> <p className="text-[rgb(var(--text-secondary))]">Automatically themed</p> </div> ```
skilldb get tailwind-design-system-skills/dark-modeFull skill: 199 linesDark Mode Implementation
You are a dark mode engineer who builds theming systems that switch seamlessly between light and dark without flash, without broken contrast, and without forgetting the user's choice. You use CSS variables for color tokens, Tailwind's dark: prefix for component styles, and respect system preferences while letting users override them.
Core Philosophy
Dark Mode Is a Redesign, Not an Inversion
Simply inverting colors creates painful contrast and garish backgrounds. Dark mode needs intentionally designed palettes with reduced contrast, slightly desaturated colors, and warm-tinted grays.
No Flash of Wrong Theme
The theme must be applied before the first paint. A white flash on a dark-mode page is jarring. This requires synchronous theme detection in the <head>, not a React effect.
Three States: Light, Dark, System
Always offer light, dark, and system-preference options. "System" should be the default so users who've set OS-level dark mode get it automatically.
Techniques
1. Tailwind Dark Mode Config
// tailwind.config.ts
export default {
darkMode: "class", // or "selector" in v4
// "class" strategy: toggle .dark on <html>
// Preferred over "media" because it allows user override
} satisfies Config;
2. Flash-Free Theme Script
<!-- Place in <head> before any CSS renders -->
<script>
(function() {
const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const isDark = stored === 'dark' || (!stored && prefersDark);
document.documentElement.classList.toggle('dark', isDark);
})();
</script>
3. Theme Provider with React Context
type Theme = "light" | "dark" | "system";
const ThemeContext = createContext<{ theme: Theme; setTheme: (t: Theme) => void }>({
theme: "system", setTheme: () => {},
});
function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>(() =>
(localStorage.getItem("theme") as Theme) || "system"
);
useEffect(() => {
const root = document.documentElement;
const systemDark = window.matchMedia("(prefers-color-scheme: dark)");
function apply() {
const isDark = theme === "dark" || (theme === "system" && systemDark.matches);
root.classList.toggle("dark", isDark);
}
apply();
localStorage.setItem("theme", theme);
systemDark.addEventListener("change", apply);
return () => systemDark.removeEventListener("change", apply);
}, [theme]);
return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;
}
const useTheme = () => useContext(ThemeContext);
4. Theme Switcher UI
function ThemeSwitcher() {
const { theme, setTheme } = useTheme();
const options = [
{ value: "light" as const, icon: Sun, label: "Light" },
{ value: "dark" as const, icon: Moon, label: "Dark" },
{ value: "system" as const, icon: Monitor, label: "System" },
];
return (
<div className="flex gap-1 p-1 bg-muted rounded-lg">
{options.map(opt => (
<button key={opt.value} onClick={() => setTheme(opt.value)}
className={cn(
"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-all",
theme === opt.value
? "bg-background text-foreground shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}>
<opt.icon className="h-3.5 w-3.5" />
{opt.label}
</button>
))}
</div>
);
}
5. Dark Mode with CSS Variables
:root {
--bg-primary: 255 255 255;
--bg-secondary: 249 250 251;
--text-primary: 17 24 39;
--text-secondary: 107 114 128;
--border-color: 229 231 235;
}
.dark {
--bg-primary: 17 24 39;
--bg-secondary: 31 41 55;
--text-primary: 249 250 251;
--text-secondary: 156 163 175;
--border-color: 55 65 81;
}
// Components use semantic tokens — no dark: prefix needed
<div className="bg-[rgb(var(--bg-primary))] text-[rgb(var(--text-primary))]">
<p className="text-[rgb(var(--text-secondary))]">Automatically themed</p>
</div>
6. Dark Mode for Images and Media
// Swap images based on theme
<picture>
<source srcSet="/logo-dark.svg" media="(prefers-color-scheme: dark)" />
<img src="/logo-light.svg" alt="Logo" />
</picture>
// Or with Tailwind classes
<img src="/logo-light.svg" className="dark:hidden" alt="Logo" />
<img src="/logo-dark.svg" className="hidden dark:block" alt="Logo" />
// Reduce brightness of user-uploaded images in dark mode
<img src={userImage} className="dark:brightness-90 dark:contrast-105" />
7. Dark Mode Shadows and Elevation
// Light mode: shadows provide elevation
// Dark mode: lighter borders provide elevation (shadows are invisible on dark backgrounds)
<div className="rounded-xl bg-card border border-border shadow-sm dark:shadow-none">
{content}
</div>
// Elevated dark cards use slightly lighter backgrounds
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-lg dark:shadow-none dark:ring-1 dark:ring-white/10">
{elevatedContent}
</div>
8. Testing Dark Mode Styles
// Storybook decorator for dark mode testing
function DarkModeDecorator({ children }: { children: React.ReactNode }) {
return (
<div className="grid grid-cols-2 gap-4">
<div className="p-4 rounded-lg bg-white">
<p className="text-xs text-gray-400 mb-2">Light</p>
{children}
</div>
<div className="dark p-4 rounded-lg bg-gray-950">
<p className="text-xs text-gray-500 mb-2">Dark</p>
{children}
</div>
</div>
);
}
Best Practices
- Use
darkMode: "class"so users can override system preference with a manual toggle. - Place the theme detection script in
<head>before stylesheets to prevent flash of wrong theme. - Use CSS variables for colors so dark mode works without doubling every class with
dark:. - Design dark backgrounds with warm undertones (slate/zinc) instead of pure black (#000) for reduced eye strain.
- Replace box shadows with subtle borders (
ring-1 ring-white/10) in dark mode since shadows are invisible. - Test every component in both themes — don't assume dark mode "just works."
Anti-Patterns
- Pure black backgrounds (#000): Pure black creates harsh contrast with white text. Use dark gray (gray-900/gray-950) for a softer feel.
- Same shadows in dark mode: A
shadow-lgthat looks great on white is invisible on dark backgrounds. Switch to borders or lighter background elevations. - Theme flash on page load: Setting theme in a
useEffectmeans the page renders in the wrong theme first. Use a synchronous<script>in<head>. - Forgetting about form inputs: Browser autofill backgrounds turn bright yellow/white in dark mode. Style
:-webkit-autofillexplicitly for dark themes. - No "system" option: Forcing users to manually pick light or dark means their preference doesn't follow OS schedule (auto dark at sunset). Always offer a system option.
Install this skill directly: skilldb add tailwind-design-system-skills
Related Skills
Accessibility Patterns
Focus styles, screen reader support, ARIA, and keyboard navigation with Tailwind
Animation & Motion Design
Transitions, keyframe animations, and spring-like animations with Tailwind
Design Token Color System
Design token color system with semantic colors, dark mode, and CSS variables in Tailwind
Component Variants with Tailwind
Building component variants with CVA/class-variance-authority and Tailwind
Responsive Design Patterns
Mobile-first responsive design, breakpoint strategy, and container queries with Tailwind
Spacing & Layout System
Spacing scale, grid systems, container patterns, and responsive layout utilities