First, why do we need to implement dark and light themes in our projects?
Having this feature is not a requirement that will necessarily bring more traffic to your project, but it greatly improves the user experience, as people have different preferences.
With that understood, let's dive into how to do it! 😄
Understanding the feature in Tailwind
To configure this feature with Tailwind, you just need to add or remove the dark
class on the element and use the
dark:
prefix in the elements’ classes that you want to style differently based on the theme.
Example:
In this example, the default background color will be
bg-gray-800
(light mode), and when the dark theme is active, it will switch to bg-gray-500
.
How it works in Next.js
In Next.js, it’s a bit different, because you’ll likely need some help from a library called
next-themes
.
Important: it’s not mandatory — you can achieve the same functionality manually — but using the library makes the development process much easier.
Let’s see examples both without and with the library.
Example without using the library
In this case, you are responsible for:
- Managing the theme value in
localStorage
; - Detecting the user’s system theme to provide a better experience.
// hooks/useTheme.ts
import { useEffect, useState } from 'react';
type Theme = 'light' | 'dark';
export function useTheme() {
const [theme, setTheme] = useState('light');
// Detect system preferences
useEffect(() => {
const savedTheme = localStorage.getItem('theme') as Theme | null;
if (savedTheme) {
setTheme(savedTheme);
} else {
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
setTheme(prefersDark ? 'dark' : 'light');
}
}, []);
// Update class and save to localStorage
useEffect(() => {
const root = document.documentElement;
if (theme === 'dark') {
root.classList.add('dark');
root.classList.remove('light');
} else {
root.classList.add('light');
root.classList.remove('dark');
}
localStorage.setItem('theme', theme);
}, [theme]);
return { theme, setTheme };
}
Example using the next-themes
library
next-themes
library
// provider/theme-provider.ts
'use client'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import * as React from 'react'
function ThemeProvider({ children, ...props }: React.ComponentProps) {
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return <>{children}>
}
return {children}
}
export { ThemeProvider }
And using it in your root layout:
async function RootLayout({ children }: { children: ReactNode }) {
return (
{children}
)
}
When using the
next-themes
library, you get many built-in features like:
-
defaultTheme="system"
, which automatically picks up the user’s system preference; - Managing theme changes through the
class
attribute; - Other customizations available in the official documentation: 👉 next-themes on npm
Conclusion
Having dark and light themes in your system is not absolutely critical, but it is very important to improve usability and the overall user interaction with your project.