Next.js + Tailwind CSS makes it easy to implement dark mode using the dark variant. After basic configurations, you can prepend dark:
to properties to indicate that they are for dark mode.
For example, if you are styling a paragraph and want the text to have the white color in dark mode and black in light mode. You’d do this:
<p className="text-white dark:text-black">Hello world!</p>
This only works if you have added darkMode: 'class'
in your tailwind.config.js
file:
// tailwind.config.ts
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: 'class',
// ...
};
Tailwind does not include dark mode styles by default, so you must enable it explicitly. It tells Tailwind CSS to use a CSS class to apply dark mode styles.
Also, using the class
strategy allows you to control dark mode by toggling a class (dark
) on the root element (usually the <html>
or <body>
tag). This allows you to programmatically add or remove the dark
class based on user actions or preferences.
Next, let’s implement the dark mode feature and switch in your Next.js project.
There are two major ways: using a package that makes it easier or writing some boilerplate code, which may not always be fun. This guide explains both methods. Let’s get right into it!
Method 1: Using the next-themes package
The next-themes
package makes managing themes in a Next.js project very easy.
It adapts to the user's system theme configuration, reduces the code you need to write, and handles theme persistence automatically.
To get started, install the package by running the command below:
npm i next-themes
Next, create a providers.tsx
file inside your app
folder and add the following code to initialize and configure next-themes
to manage theme switching in your Next.js application:
'use client';
import { ThemeProvider } from 'next-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
);
}
This code imports ThemeProvider
from next-themes
, and the Providers
component wraps the entire application, enabling theme management with CSS class toggling on the HTML element.
The configuration sets the default theme to match the user's system preference and allows automatic theme switching based on system settings, ensuring consistent and responsive theme management throughout the app.
Next, add the <Providers>
component to your root layout.tsx
by placing it inside the <body>
tag:
import { Providers } from './providers';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Notice the suppressHydrationWarning
setting inside the <html>
tag. This prevents hydration warnings because the html
element is updated by next-themes
.
Next, you can add styles that differentiate your project's dark and light modes. For example, you can style the general page via the <body>
tag in the layout.tsx
file:
import { Providers } from './providers';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body className={`bg-lightbg text-black dark:bg-darkbg dark:text-white`}>
<Providers>{children}</Providers>
</body>
</html>
);
}
Finally, create a component to toggle between light and dark modes:
'use client';
import { useState, useEffect } from 'react';
import { useTheme } from 'next-themes';
const ThemeSwitcher = () => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<div className="bg-background text-primary-green">
The current theme is: {theme}
<br />
<button onClick={() => setTheme('light')}>Light Mode</button>
<br />
<button onClick={() => setTheme('dark')}>Dark Mode</button>
</div>
);
};
export default ThemeSwitcher;
This component tracks its mounted state to avoid hydration mismatch warnings. When mounted, it renders a div
displaying the current theme and provides buttons to switch between light and dark modes by calling setTheme
.
Method 2: Implement light & dark mode from scratch
If you prefer not to use external packages, you can manage dark mode manually with some custom boilerplate code.
To do this, ensure you have added darkMode: 'class'
in your tailwind.config.js
file.
Next, create a component that will handle the entire logic and add the following code:
'use client';
import { useState, useEffect, use } from 'react';
const ThemeToggle = () => {
const [darkTheme, setDarkTheme] = useState(true);
useEffect(() => {
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
setDarkTheme(true);
} else {
setDarkTheme(false);
}
}, []);
useEffect(() => {
if (darkTheme) {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
}, [darkTheme]);
return (
<div>
<button onClick={() => setDarkTheme(!darkTheme)}>Toggle Theme</button>
</div>
);
};
export default ThemeToggle;
Let’s break down the code above and explain it in bits.
In the ThemeToggle component, we use the state hook to manage the dark theme state, determining whether the dark mode is enabled.
const [darkTheme, setDarkTheme] = useState(true);
By default, we set it to true
, assuming dark mode is on initially. Next, we use the useEffect
hook to check the user's previously saved theme from local storage when the component mounts:
useEffect(() => {
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
setDarkTheme(true);
} else {
setDarkTheme(false);
}
}, []);
This effect runs once when the component mounts, retrieving the theme preference from local storage. If the saved theme is 'dark', we set darkTheme
to true
; otherwise, we set it to false
. This ensures the user's theme preference is respected when they revisit the site.
Another useEffect
hook listens for changes to the darkTheme
state. Whenever it changes, this effect updates the html
element's class to either add or remove the 'dark' class and saves the current theme to local storage:
useEffect(() => {
if (darkTheme) {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
}, [darkTheme]);
This ensures that the UI updates reflect the current theme and that the theme preference is saved for future visits. Finally, we provide a simple button that toggles the theme when clicked:
return (
<div>
<button onClick={() => setDarkTheme(!darkTheme)}>Toggle Theme</button>
</div>
);
The button toggles the darkTheme
state, switching between light and dark modes.
Wrapping up!
We've explored two methods for implementing light and dark modes in a Next.js application using Tailwind CSS.
The next-themes
package offers a quick and efficient way to manage themes, automatically adapts to system settings, and requires minimal code. It's ideal if you want a reliable, hassle-free setup that seamlessly integrates with Tailwind CSS.
Choose next-themes
for ease and reliability or a custom solution for flexibility and control. Both methods ensure a smooth light and dark mode experience for your users.