TailwindCSS v4 no longer maintains multiple config files-everything now goes inside a single global.css file. This can make theming feel a bit challenging. In this guide, I'll show you how to easily set up light, dark, and even custom themes in your Next.js project using next-themes.
Step 01: Initiate your project
π Install your Next.js project
pnpm create next-app my-project-name
pnpm install
pnpm dev
π Install Next Themes
pnpm add next-themes
Step 02: Modify your layout.tsx
π Wrap the application(children) with <ThemeProvider></ThemeProvider>. In the following example, I keep all the default Next.js code.
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { ThemeProvider } from "next-themes";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}
π Add attributes for ThemeProvider.
<ThemeProvider enableSystem={true} defaultTheme="system" >
{children}
</ThemeProvider>
Attributes
enableSystem={true} ensures that your application can perform according to the user's device preference. By default, it is false.
defaultTheme="system" defines which theme will be loaded on the first load of the application.
error
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:
Add suppressHydrationWarning attribute in html tag. It will solve the error.
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<ThemeProvider enableSystem={true} defaultTheme="system">
{children}
</ThemeProvider>
</body>
</html>
Remember that ThemeProvider is not a server component. It is a client component.
Step 03: Customize your color
π global.css
Add them to your global.css
@import 'tailwindcss';
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
To work with tailwindcss v4, @import 'tailwindcss' is required.
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)) line of code enables dark classes in HTML tags.
<div className="bg-gray-300 dark:bg-amber-500">
<h2 className="text-xl text-teal-600">Next Theme With Tailwindcss v4
</h2>
</div>
When the dark theme is selected, the style applied for dark mode will be applied. In the example, bg-amber-500 will be applied for the dark theme.
Now add colors variables as you prefer.
@theme {
/* theme background color */
/* Light mode default */
--bg-color-light-default: hsl(220, 14%, 96%);
/* Dark mode default */
--bg-color-dark-default: hsl(207, 95%, 8%);
}
Use the theme, directive as you use in tailwindcss.
Add :root selector for each theme. The application will maintain background colors from the root of your application.
:root[data-theme="light"] {
background-color: var(--color-background-light);
}
:root[data-theme="dark"] {
background-color: var(--color-background-dark);
}
Step 04: Add theme toggler button
To change different themes, I am going to use lucid react icons library.
As it is a user's interactive button, it will be a client component. I make it different component for it, named ThemetogglerBtn.tsx.
pnpm install lucide-react
ThemetogglerBtn.tsx
'use client';
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import { Sun, Moon } from 'lucide-react';
export default function ThemeSwitcher() {
const [mounted, setMounted] = useState(false);
const { theme, setTheme, resolvedTheme } = useTheme();
useEffect(() => {
setMounted(true);
},
[]);
const toggleTheme = () => {
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')
}
if (!mounted) {
return (
<div className="w-10 h-10 flex items-center justify-center rounded-full bg-gray-200 dark:bg-gray-700">
</div>
)
}
const currentIcon = resolvedTheme === 'dark' ? (<Sun size={24} className="text-yellow-500" />) :
(<Moon size={24} strokeWidth={2} className="text-gray-700"/>);
return (
<button
onClick={toggleTheme}
className="p-2 rounded-full flex items-center justify-center transition-colors bg-[--bg-light] hover:bg-[--bg-light] dark:hover:bg-[--bg-light]"
aria-label={resolvedTheme === 'dark' ? "Switch to light theme" : "Switch to dark theme"}
title={resolvedTheme === 'dark' ? 'Switch to light theme' : 'Switch to dark theme'}
>
{currentIcon}
<span className="sr-only">Theme switcher button</span>
</button>
)
};
π useTheme hook
import use-theme() from next-themes
const {theme, setTheme, resolvedTheme = useTheme()
π theme shows the current selected theme.
π setTheme, with the setter function, we can set a new theme.
π resolvedTheme: It acknowledges the system presence theme. Currently, is it darkor light theme active on the user's device?
π mounted state
In next.js useTheme may mismatch hydration for client and server side components. To avoid this, we use mounted state.
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true)
},[])
- Initially
mounted = false - After rendering
clientcomponent, it becomestrue - So it ensures
theme togglerworks only inclient side.
π toggleTheme theme toggler function
const toggleTheme =() => {
setTheme(resolvedTheme = 'dark' ? 'light' : 'dark');
}
- If the current value of
resolvedTheme === dark, then it will change the theme tolight. And ifresolvedTheme === light, it will change todark.
π Change icon
const currentIcon = resolvedTheme === `dark` ?
(<Sun size={24} className='text-yellow-500' />)
: <Moon size={24} className='text-gray-700'/>)
- If
darkmode is enabled, it will renderSunicon. And forlightmodeMoonicon.
π UI in button
<button onClick={toggleTheme}>
{currentIcon}
<span className="sr-only">Theme switcher button</span>
</button>
- Clicking on the button activates
toggleThemefunction. -
{currentIcon}:rendersicon. -
<span className="sr-only">Theme switcher button</span>:sr-onlyis a good practice for accessibility.
β At a glance
- Install
next-themes - Wrap app with
<ThemeProvider> - Configure
global.csswith@custom-variant dark - Add theme toggler button with
useThemehook - Enjoy smooth dark/light theme switching π
Top comments (0)