Why Dark Mode Matters
Dark mode has evolved from a trendy feature to an accessibility necessity. Users with photosensitivity, migraines, or visual impairments rely on dark mode to reduce eye strain. In low-light environments, it dramatically improves readability while saving battery on OLED screens.
According to research, 82% of smartphone users prefer dark mode, making it one of the highest-impact UX improvements you can make.
The Architecture: CSS Variables + next-themes
The cleanest approach to dark mode in Next.js uses two layers: CSS variables for theme-aware colors, and next-themes for managing the active theme class. This separation means your components don't need conditional logic — they just use semantic CSS variables, and the CSS handles the rest.
Setting Up next-themes
Create a ThemeProvider component that wraps NextThemesProvider with attribute set to 'class', defaultTheme set to 'system', and enableSystem enabled. Wrap your root layout with this provider. The attribute='class' setting tells next-themes to add a dark class to the html element rather than using data attributes.
CSS Variables for Theming
Define your color palette as CSS variables under :root for light mode and under .dark for dark mode. Use semantic names like --bg, --fg, --border, and --accent rather than specific color names. This semantic layer means you can completely change your color palette by editing just the CSS variables.
Building the Theme Toggle
The theme toggle button needs to handle the 'system' preference state gracefully. Use the useTheme hook from next-themes inside a client component. Critically, you must handle the mounting state to avoid hydration mismatches — return null or a placeholder until the component has mounted on the client.
Avoiding the Hydration Flash
The most common dark mode bug is a white flash on page load for dark mode users. Solve it by setting suppressHydrationWarning on the html tag, which prevents React from complaining about class mismatches between server and client, and by using next-themes which injects a script before React hydrates to set the correct class immediately.
Accessibility Considerations
Good dark mode implementation includes sufficient contrast ratios (WCAG 2.1 requires 4.5:1 for normal text), not inverting images (photographs look wrong when inverted), respecting system preference as the default, keyboard-accessible toggle buttons, and announcing theme changes for screen readers.
Common Mistakes to Avoid
Avoid hardcoded colors — always use CSS variables. Don't forget images — add appropriate filters or provide dark mode variants. Don't remove focus styles in dark mode. Add a smooth transition to avoid abrupt color changes. Test with the explicit toggle, not just your OS preference.
Conclusion
A well-implemented dark mode shows respect for your users' preferences and needs. By using CSS variables as a semantic layer, next-themes for preference management, and keeping accessibility top of mind, you can build a dark mode that delights users without complexity.