A complete guide to dark mode implementation — from color tokens to system preference detection to persistent user choice.
Dark Mode Is Not Just Inverting Colors
The biggest mistake in dark mode implementations is thinking you can just swap white backgrounds to black and call it done. Good dark mode requires a separate color palette designed for dark surfaces.
Color Strategy
Dark mode colors need to account for:
- Reduced contrast — pure white (#fff) on pure black (#000) is harsh. Use off-white (#e5e5e5) on dark gray (#0a0a0a)
- Elevated surfaces — use slightly lighter backgrounds for cards and modals to create depth
- Accent colors — some vibrant colors need to be desaturated or lightened to remain accessible on dark backgrounds
- Shadows — dark mode shadows barely work on dark backgrounds; use subtle borders or lighter elevated surfaces instead
CSS Custom Properties Approach
Define your colors as CSS variables and override them per theme:
:root {
--bg: #ffffff;
--text: #1a1a1a;
--border: #e5e5e5;
}
[data-theme="dark"] { --bg: #0a0a0a; --text: #e5e5e5; --border: #1e1e1e; }
Every component references these variables. Switching themes updates everything instantly.
Detecting System Preference
CSS can detect the user's OS theme preference:
@media (prefers-color-scheme: dark) {
:root { / dark values / }
}
Persistent User Choice
Use JavaScript to store the user's explicit choice in localStorage. The priority order should be: explicit user choice, then system preference, then your default.
Avoiding the Flash
The flash of wrong theme on page load happens because JavaScript runs after HTML renders. Solutions:
- Use a blocking
in thethat reads localStorage and setsdata-themebefore the page paints - Or use
color-scheme: darkin CSS to tell the browser the page is dark by default
Images in Dark Mode
Some images look bad on dark backgrounds. Options:
- Add a subtle border or background behind images
- Use
filter: brightness(0.85)to dim images slightly in dark mode - Provide separate dark-optimized image variants for logos and illustrations
Testing
Test dark mode with screenshots and automated visual regression. It is easy to miss one component that still has a hardcoded white background.