Dark Mode Done Right
A practical guide to implementing dark mode that doesn't hurt your eyes or your codebase.
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:
#0a0a0afor the main background#121212for slightly elevated surfaces#1a1a1afor 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.