DEV Community

Cover image for Next.js + TailwindCSS v4: How to Add Dark/Light Theme with Next-Themes
Khan Rabiul
Khan Rabiul

Posted on

Next.js + TailwindCSS v4: How to Add Dark/Light Theme with Next-Themes

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
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Install Next Themes

pnpm add next-themes
Enter fullscreen mode Exit fullscreen mode

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>
  );
}

Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ Add attributes for ThemeProvider.

<ThemeProvider enableSystem={true} defaultTheme="system" >
{children}
</ThemeProvider>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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] *));
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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%);  
}
Enter fullscreen mode Exit fullscreen mode

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);
}

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
  )
};
Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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)
},[])
Enter fullscreen mode Exit fullscreen mode
  • Initially mounted = false
  • After rendering client component, it becomes true
  • So it ensures theme toggler works only in client side.
πŸŒ— toggleTheme theme toggler function
const toggleTheme =() => {
setTheme(resolvedTheme = 'dark' ? 'light' : 'dark');
}
Enter fullscreen mode Exit fullscreen mode
  • If the current value of resolvedTheme === dark, then it will change the theme to light. And if resolvedTheme === light, it will change to dark.
πŸŒ— Change icon
const currentIcon = resolvedTheme === `dark` ?
(<Sun size={24} className='text-yellow-500' />) 
: <Moon size={24} className='text-gray-700'/>)
Enter fullscreen mode Exit fullscreen mode
  • If dark mode is enabled, it will render Sun icon. And for light mode Moon icon.
πŸŒ— UI in button
<button onClick={toggleTheme}>
{currentIcon}
 <span className="sr-only">Theme switcher button</span>
</button>
Enter fullscreen mode Exit fullscreen mode
  • Clicking on the button activates toggleTheme function.
  • {currentIcon}: renders icon.
  • <span className="sr-only">Theme switcher button</span>: sr-only is a good practice for accessibility.

βœ… At a glance

  1. Install next-themes
  2. Wrap app with <ThemeProvider>
  3. Configure global.css with @custom-variant dark
  4. Add theme toggler button with useTheme hook
  5. Enjoy smooth dark/light theme switching πŸŽ‰

Top comments (0)