Implementing Dark Mode the Right Way
Dark mode is no longer a nice-to-have — it’s an expected feature. But implementing it correctly requires more than just inverting colors. Let’s build a dark mode implementation that’s performant, accessible, and delightful to use.
The FOUC Problem
The biggest challenge with dark mode is preventing the Flash of Unstyled Content (FOUC). When a user with a dark preference visits your site, they shouldn’t see a white flash before the theme applies. The solution is an inline script in the <head>:
<script is:inline>
(function() {
const stored = localStorage.getItem('theme');
if (stored === 'dark' || (!stored && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.setAttribute('data-theme', 'dark');
}
})();
</script>
This runs before any rendering, so the correct theme is applied instantly.
CSS Custom Properties for Theming
The cleanest approach uses CSS custom properties on the root element:
:root {
--bg: #fafbfc;
--text: #1a1a2e;
--accent: #6366f1;
}
[data-theme="dark"] {
--bg: #0f0f1a;
--text: #e8e8ed;
--accent: #818cf8;
}
Switch themes by toggling the data-theme attribute. All components automatically inherit the new values.
Beyond Simple Inversion
Don’t just invert your light theme. Dark mode requires thoughtful design decisions:
- Reduce brightness, not just swap — Dark backgrounds shouldn’t be pure black. Use deep navy or charcoal instead
- Adjust saturation — Colors often need desaturation in dark mode to reduce visual vibration
- Elevate surfaces — Use progressively lighter backgrounds for elevated elements (cards, modals)
- Reduce shadows — Shadows are less visible on dark backgrounds. Use subtle borders or background differentiation instead
Image Handling in Dark Mode
Images can be jarring in dark mode. Consider these approaches:
[data-theme="dark"] .hero-image {
filter: brightness(0.85);
}
[data-theme="dark"] .diagram {
filter: invert(1) hue-rotate(180deg);
}
The first reduces brightness for photos; the second inverts diagrams while preserving color relationships.
Smooth Transitions
Add transitions for theme changes, but be selective — you don’t want every element to animate:
body {
transition: background-color 0.3s ease, color 0.3s ease;
}
Only transition the body’s background and text color. Other elements inherit these values and update instantly.
Respecting User Preferences
Always start with the user’s system preference and allow manual override:
@media (prefers-color-scheme: dark) {
[data-theme="default"] {
--bg: #0f0f1a;
--text: #e8e8ed;
}
}
The default theme follows the system, while light and dark provide explicit overrides. This three-option approach gives users maximum control.
Implementing dark mode well is about respecting your users’ preferences and creating a comfortable reading experience in any lighting condition. Take the time to get it right — your users will thank you.