Dark Mode Done Right: A Technical and Design Guide
Beyond inverting colors — implementing dark mode with CSS custom properties, system preference detection, and accessible contrast ratios.
Dark mode is no longer optional — users expect it. But implementing dark mode is not as simple as inverting colors. True dark mode requires a systematic approach: semantic color tokens, proper contrast ratios, adjusted shadows and elevations, and image handling. Done wrong, dark mode causes eye strain, illegible text, and accessibility failures.
Semantic Color Tokens
The foundation of dark mode is semantic color tokens — variables named by purpose, not value. Instead of --color-gray-900 and --color-white, use --color-bg-primary and --color-text-primary. In light mode, --color-bg-primary resolves to white; in dark mode, to a dark gray. Components reference semantic tokens and automatically adapt to both themes.
:root {
/* Light theme (default) */
--color-bg-primary: #ffffff;
--color-bg-secondary: #f8fafc;
--color-bg-elevated: #ffffff;
--color-text-primary: #0f172a;
--color-text-secondary: #64748b;
--color-border: #e2e8f0;
--color-shadow: rgba(0, 0, 0, 0.08);
}
[data-theme="dark"] {
--color-bg-primary: #0f172a;
--color-bg-secondary: #1e293b;
--color-bg-elevated: #1e293b; /* Elevated surfaces are lighter in dark mode */
--color-text-primary: #f1f5f9;
--color-text-secondary: #94a3b8;
--color-border: #334155;
--color-shadow: rgba(0, 0, 0, 0.3);
}
/* Respect system preference */
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg-primary: #0f172a;
/* ... dark values ... */
}
}Dark Mode Design Principles
- Don't use pure black (#000000): It creates harsh contrast against colored text. Use dark gray (#0f172a, #1a1a2e) for backgrounds.
- Reduce white text opacity: Instead of pure white (#ffffff) text, use slightly muted white (#f1f5f9) to reduce eye strain.
- Invert elevation model: In light mode, higher surfaces are white with shadows. In dark mode, higher surfaces are lighter grays — shadows are invisible on dark backgrounds.
- Desaturate brand colors: Bright saturated colors on dark backgrounds cause vibration and eye strain. Reduce saturation by 10-20% for dark mode.
- Handle images: Add a subtle dark overlay to light images, or provide dark-mode-specific image variants for illustrations and diagrams.
Theme Persistence and System Sync
export function useTheme() {
const [theme, setTheme] = useState<'light' | 'dark' | 'system'>(() => {
if (typeof window === 'undefined') return 'system';
return (localStorage.getItem('theme') as 'light' | 'dark') || 'system';
});
useEffect(() => {
const root = document.documentElement;
if (theme === 'system') {
root.removeAttribute('data-theme');
localStorage.removeItem('theme');
} else {
root.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}
}, [theme]);
return { theme, setTheme };
}Always provide three options: Light, Dark, and System (auto). Many users want their apps to follow their OS preference, which changes automatically at sunrise/sunset.
Dark mode is a design feature, not a CSS hack. Invest in a proper token system, test contrast ratios (WCAG AA minimum: 4.5:1 for text), and treat dark mode as a first-class design language alongside your light theme.
Emily Nakamura
Design Systems Lead