How to Implement Dark Mode in Next.js with CSS Variables

React Specialist
December 1, 2024
Updated on December 3, 2024
0 MIN READ
#react#architecture#implement#dark

Introduction

Dark mode has become a popular feature in modern web applications, improving readability and reducing eye strain in low-light environments. Implementing dark mode in Next.js is straightforward when leveraging CSS variables (custom properties) for theming. This approach ensures maintainability, performance, and seamless transitions between themes.

In this guide, we'll walk through implementing dark mode in a Next.js application using CSS variables, React context for state management, and localStorage for persistence.

Setting Up CSS Variables for Theming

CSS variables allow us to define theme-specific values that can be dynamically updated with JavaScript. We'll create a global CSS file to define our light and dark theme variables.

First, create a globals.css file (or modify your existing one) in the styles directory:

:root {
  --background: #ffffff;
  --text: #000000;
  --primary: #0070f3;
  --secondary: #f5f5f5;
}

[data-theme="dark"] {
  --background: #121212;
  --text: #ffffff;
  --primary: #90caf9;
  --secondary: #1e1e1e;
}

Here, we define two sets of variables:

  • The default (:root) represents the light theme.
  • The [data-theme="dark"] selector overrides these variables for the dark theme.

Apply these variables in your components:

body {
  background-color: var(--background);
  color: var(--text);
  transition: background-color 0.3s ease, color 0.3s ease;
}

Managing Theme State with React Context

To toggle between themes, we'll use React context to manage the theme state globally.

  1. Create a Theme Context:

    In a new file, e.g., ThemeContext.js:

import { createContext, useContext, useState, useEffect } from 'react'; const ThemeContext = createContext(); export const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light')); }; useEffect(() => { // Apply theme to the document document.documentElement.setAttribute('data-theme', theme); // Save to localStorage localStorage.setItem('theme', theme); }, [theme]); // Check for saved theme preference on load useEffect(() => { const savedTheme = localStorage.getItem('theme'); if (savedTheme) { setTheme(savedTheme); } }, []); return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); }; export const useTheme = () => useContext(ThemeContext);
  1. Wrap Your App with the Theme Provider:

    In _app.js:

import { ThemeProvider } from '../context/ThemeContext'; function MyApp({ Component, pageProps }) { return ( <ThemeProvider> <Component {...pageProps} /> </ThemeProvider> ); } export default MyApp;

Implementing a Theme Toggle Button

Now, let's create a button to switch between themes.

import { useTheme } from '../context/ThemeContext'; const ThemeToggle = () => { const { theme, toggleTheme } = useTheme(); return ( <button onClick={toggleTheme}> {theme === 'light' ? '🌙 Dark Mode' : '☀️ Light Mode'} </button> ); }; export default ThemeToggle;

Place this component anywhere in your app (e.g., in a header or navbar).

Handling Server-Side Rendering (SSR)

Next.js renders pages on the server, which can cause a flash of incorrect theme (FOUC) if the theme isn't synced between server and client. To fix this, we'll use a useEffect hook to apply the theme only after hydration.

Modify the ThemeProvider to prevent theme mismatch:

useEffect(() => { const savedTheme = localStorage.getItem('theme'); const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light'); setTheme(initialTheme); }, []);

For better UX, you can also listen for system theme changes:

useEffect(() => { const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); const handleChange = (e) => { setTheme(e.matches ? 'dark' : 'light'); }; mediaQuery.addEventListener('change', handleChange); return () => mediaQuery.removeEventListener('change', handleChange); }, []);

Conclusion

Implementing dark mode in Next.js with CSS variables is a clean and efficient approach. By using React context for state management and localStorage for persistence, we ensure a seamless user experience. The use of CSS variables makes theme switching performant with smooth transitions.

Key takeaways:

  • Define theme variables in CSS for easy maintenance.
  • Use React context to manage and toggle themes globally.
  • Persist user preferences with localStorage.
  • Handle SSR issues to prevent theme mismatch flashes.

With these steps, you can enhance your Next.js application with a polished dark mode feature that users will appreciate.

Share this article