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:

  1. Reduce brightness, not just swap — Dark backgrounds shouldn’t be pure black. Use deep navy or charcoal instead
  2. Adjust saturation — Colors often need desaturation in dark mode to reduce visual vibration
  3. Elevate surfaces — Use progressively lighter backgrounds for elevated elements (cards, modals)
  4. 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.

Related Posts

Type to search across blog posts and documentation