Next.js Theme Switching
Theme switching has become an essential feature in modern web applications. Users expect the ability to toggle between light and dark modes based on their preferences or system settings. In this tutorial, we'll learn how to implement theme switching in a Next.js application using various approaches.
Introduction to Theme Switchingā
Theme switching involves dynamically changing the color scheme and visual appearance of your application. Common scenarios include:
- Switching between light and dark modes
- Supporting user-preferred color schemes
- Respecting system preferences
- Storing user preferences for future visits
Next.js offers several ways to implement theme switching, from CSS variables to more sophisticated solutions involving context APIs and localStorage.
Basic Theme Switching with CSS Variablesā
The simplest approach uses CSS variables (custom properties) to define different color schemes.
Step 1: Define CSS Variablesā
Create a global CSS file (e.g., globals.css
in the styles
directory):
:root {
--background: #ffffff;
--text-primary: #333333;
--text-secondary: #666666;
--accent: #0070f3;
}
[data-theme='dark'] {
--background: #121212;
--text-primary: #ffffff;
--text-secondary: #e0e0e0;
--accent: #2196f3;
}
body {
background-color: var(--background);
color: var(--text-primary);
transition: all 0.3s ease;
}
Step 2: Create a Theme Toggle Componentā
// components/ThemeToggle.js
import { useState, useEffect } from 'react';
export default function ThemeToggle() {
const [theme, setTheme] = useState('light');
// Initialize theme from localStorage or system preference
useEffect(() => {
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme) {
setTheme(savedTheme);
} else if (prefersDark) {
setTheme('dark');
}
}, []);
// Update the document and save preference when theme changes
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light');
};
return (
<button
onClick={toggleTheme}
className="theme-toggle"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
{theme === 'light' ? 'š' : 'āļø'}
</button>
);
}
Step 3: Add the Toggle to Your Layoutā
// components/Layout.js
import ThemeToggle from './ThemeToggle';
export default function Layout({ children }) {
return (
<div className="layout">
<header>
<nav>
<h1>My Next.js App</h1>
<ThemeToggle />
</nav>
</header>
<main>{children}</main>
</div>
);
}
Advanced Theme Switching with Context APIā
For more complex applications, using React's Context API provides better state management and theme accessibility across components.
Step 1: Create a Theme Contextā
// contexts/ThemeContext.js
import { createContext, useState, useEffect, useContext } from 'react';
const ThemeContext = createContext({
theme: 'light',
setTheme: () => null,
toggleTheme: () => null,
});
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
useEffect(() => {
// Avoid hydration mismatch by running on client side only
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme) {
setTheme(savedTheme);
} else if (prefersDark) {
setTheme('dark');
}
}, []);
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// Custom hook for using the theme context
export const useTheme = () => useContext(ThemeContext);
Step 2: Wrap Your App with the Providerā
// pages/_app.js
import { ThemeProvider } from '../contexts/ThemeContext';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
);
}
export default MyApp;
Step 3: Use the Theme Context in Componentsā
// components/ThemeToggle.js
import { useTheme } from '../contexts/ThemeContext';
export default function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
className="theme-toggle"
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
>
{theme === 'light' ? 'š' : 'āļø'}
</button>
);
}
Preventing Flash of Incorrect Themeā
A common issue with theme switching is the "flash of incorrect theme" (FOIT) when the page loads. Let's fix this by adding a script to the _document.js
file:
// pages/_document.js
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head />
<body>
<script
dangerouslySetInnerHTML={{
__html: `
(function() {
function getTheme() {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
return savedTheme;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
document.documentElement.setAttribute('data-theme', getTheme());
})();
`,
}}
/>
<Main />
<NextScript />
</body>
</Html>
);
}
This script runs before React hydration and sets the correct theme immediately.
Real-World Example: Theme with Tailwind CSSā
Tailwind CSS is a popular utility-first CSS framework that works well with Next.js. Here's how to implement theme switching with Tailwind:
Step 1: Configure Tailwind for Dark Modeā
// tailwind.config.js
module.exports = {
darkMode: 'class', // Use class strategy for dark mode
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
// Custom colors if needed
},
},
plugins: [],
};
Step 2: Create a Theme Toggle Componentā
// components/ThemeToggle.js
import { useState, useEffect } from 'react';
import { SunIcon, MoonIcon } from '@heroicons/react/solid';
export default function ThemeToggle() {
const [darkMode, setDarkMode] = useState(false);
useEffect(() => {
// On page load or when changing themes, best to add inline in `head` to avoid FOUC
const isDarkMode = localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
setDarkMode(isDarkMode);
if (isDarkMode) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, []);
const toggleDarkMode = () => {
setDarkMode(!darkMode);
if (darkMode) {
localStorage.theme = 'light';
document.documentElement.classList.remove('dark');
} else {
localStorage.theme = 'dark';
document.documentElement.classList.add('dark');
}
};
return (
<button
onClick={toggleDarkMode}
className="p-2 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
aria-label={`Switch to ${darkMode ? 'light' : 'dark'} mode`}
>
{darkMode ? (
<SunIcon className="h-5 w-5 text-yellow-400" />
) : (
<MoonIcon className="h-5 w-5 text-gray-600" />
)}
</button>
);
}
Step 3: Add Tailwind Classes to Your Componentsā
// components/Layout.js
import ThemeToggle from './ThemeToggle';
export default function Layout({ children }) {
return (
<div className="min-h-screen bg-white dark:bg-gray-900 transition-colors duration-300">
<header className="border-b border-gray-200 dark:border-gray-700">
<nav className="container mx-auto px-4 py-4 flex justify-between items-center">
<h1 className="text-2xl font-bold text-gray-800 dark:text-white">My Next.js App</h1>
<ThemeToggle />
</nav>
</header>
<main className="container mx-auto px-4 py-8 text-gray-900 dark:text-gray-100">
{children}
</main>
</div>
);
}
Theme Switching with Next.js 13+ App Directoryā
Next.js 13 introduced the App Directory with improved server components. Here's how to implement theme switching in this newer architecture:
Step 1: Create a Theme Provider (Client Component)ā
// components/ThemeProvider.js
'use client';
import { createContext, useContext, useEffect, useState } from 'react';
const ThemeContext = createContext({
theme: 'light',
toggleTheme: () => {},
});
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
// Initialize theme on client side
useEffect(() => {
const savedTheme = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (savedTheme) {
setTheme(savedTheme);
} else if (prefersDark) {
setTheme('dark');
}
// Apply theme class to document
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, [theme]);
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light';
setTheme(newTheme);
localStorage.setItem('theme', newTheme);
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);
Step 2: Add the Provider to the Root Layoutā
// app/layout.js
import { ThemeProvider } from '../components/ThemeProvider';
import './globals.css';
export default function RootLayout({ children }) {
return (
<html lang="en">
<head />
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}
Step 3: Create a Theme Toggle Button (Client Component)ā
// components/ThemeToggle.js
'use client';
import { useTheme } from './ThemeProvider';
export default function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button
onClick={toggleTheme}
className="p-2 rounded-lg bg-gray-200 dark:bg-gray-800"
>
{theme === 'light' ? 'Dark Mode š' : 'Light Mode āļø'}
</button>
);
}
Step 4: Prevent Flash of Incorrect Themeā
For the App Router, we need to modify our approach slightly. Create a script component:
// components/ThemeScript.js
'use client';
import { useEffect } from 'react';
export default function ThemeScript() {
useEffect(() => {
// This runs once on client
const checkTheme = () => {
const isDark = localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
};
checkTheme();
// Optional: Listen for system preference changes
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleChange = () => {
if (!('theme' in localStorage)) {
checkTheme();
}
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
}, []);
return null;
}
Then include this script in your layout:
// app/layout.js
import { ThemeProvider } from '../components/ThemeProvider';
import ThemeScript from '../components/ThemeScript';
import './globals.css';
export default function RootLayout({ children }) {
return (
<html lang="en">
<head />
<body>
<ThemeScript />
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}
Summaryā
In this tutorial, we explored various approaches to implement theme switching in Next.js applications:
- Basic Theme Switching with CSS Variables - A straightforward approach using data attributes and CSS variables
- Advanced Theme Switching with Context API - Better state management across components
- Theme Switching with Tailwind CSS - Implementing dark mode with Tailwind's utility classes
- Theme Switching with Next.js 13+ App Directory - Using the new app directory structure with client components
Each approach has its advantages depending on your project's complexity and requirements. When implementing theme switching, remember these key points:
- Use CSS variables or Tailwind classes to style your themes
- Store user preferences in localStorage
- Respect system color scheme preferences
- Prevent flash of incorrect theme on initial load
- Make sure theme changes are visually smooth with transitions
By following these principles, you'll create a better user experience with a consistent, accessible, and customizable interface.
Additional Resources and Exercisesā
Resources:ā
Exercises:ā
- Theme System Extension: Extend the theme system to support multiple themes (not just dark/light) such as "sepia", "high contrast", etc.
- Theme Animation: Add smooth transitions and animations when switching between themes.
- Theme Settings Panel: Create a settings panel that allows users to customize individual theme elements.
- Themed Components: Create a set of components that automatically adapt to the current theme.
Happy coding with Next.js theme switching!
If you spot any mistakes on this website, please let me know at [email protected]. Iād greatly appreciate your feedback! :)