Dark Mode Done Right

A practical guide to implementing dark mode that doesn't hurt your eyes or your codebase.

cssdesigntutorial

Dark mode isn't just about inverting colors. It's about creating an experience that's comfortable to use in low-light conditions while maintaining readability and visual hierarchy.

The Foundation: CSS Variables

The cleanest approach to dark mode uses CSS custom properties. This keeps your color logic in one place and makes switching themes trivial.

:root {
  --background: #ffffff;
  --foreground: #000000;
  --accent: #0066cc;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #e0e0e0;
    --accent: #4d9fff;
  }
}

Notice how the accent color changes in dark mode. Pure blue (#0066cc) looks too intense on a dark background, so we use a lighter shade that provides the same visual weight.

Not Pure Black

One of the biggest mistakes in dark mode implementation is using pure black (#000000). It creates too much contrast with white text, causing eye strain.

Instead, use a very dark gray:

  • #0a0a0a for the main background
  • #121212 for slightly elevated surfaces
  • #1a1a1a for cards and modals

This creates subtle depth while maintaining the "dark" aesthetic.

Color Adjustments

Colors behave differently on dark backgrounds. What looks good in light mode often needs adjustment:

// Light mode: darker accent for contrast
// Dark mode: lighter accent for visibility
const theme = {
  light: {
    accent: "#6b5b95",  // Darker purple
    link: "#5a4a85",    // Even darker for links
  },
  dark: {
    accent: "#9d87d9",  // Lighter purple
    link: "#b39ddb",    // Lighter still for links
  },
};

Implementation in React

Here's a clean way to handle theme switching in Next.js:

"use client";

import { useEffect, useState } from "react";

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<"light" | "dark">("dark");

  useEffect(() => {
    const stored = localStorage.getItem("theme");
    if (stored === "light" || stored === "dark") {
      setTheme(stored);
    } else {
      // Respect system preference
      const prefersDark = window.matchMedia(
        "(prefers-color-scheme: dark)"
      ).matches;
      setTheme(prefersDark ? "dark" : "light");
    }
  }, []);

  useEffect(() => {
    document.documentElement.dataset.theme = theme;
    localStorage.setItem("theme", theme);
  }, [theme]);

  return children;
}

Respecting System Preferences

Always respect the user's system preference as the default:

@media (prefers-color-scheme: dark) {
  /* Dark mode styles */
}

@media (prefers-color-scheme: light) {
  /* Light mode styles */
}

But also provide a way to override it. Some users prefer dark mode everywhere except your site, and that's okay.

Images and Media

Don't forget about images. Some images need special handling in dark mode:

function Logo() {
  return (
    <>
      <img
        src="/logo-light.svg"
        alt="Logo"
        className="dark:hidden"
      />
      <img
        src="/logo-dark.svg"
        alt="Logo"
        className="hidden dark:block"
      />
    </>
  );
}

For photos, consider reducing opacity slightly in dark mode to prevent them from being too bright.

Testing

Test your dark mode in actual low-light conditions. What looks good under office lighting might be too bright when you're using your device at night.

Also test:

  • All interactive states (hover, active, focus)
  • Form inputs and their borders
  • Syntax highlighting in code blocks
  • Charts and data visualizations

Conclusion

Good dark mode is about more than aesthetics. It's about creating a comfortable reading experience that works in different lighting conditions. Take the time to adjust your colors, test in real conditions, and respect your users' preferences.

Your eyes (and your users' eyes) will thank you.