Search code examples
reactjsnext.jstailwind-cssthemesdarkmode

Tailwind 'dark:' not working with Next 15, next-themes, and Tailwind 4


I've got the light mode switch working, but now Tailwind's 'dark:' class modifier does not work.

Following these instructions: Implementing Dark Mode and Theme Switching using Tailwind v4 and Next.js

Reproduce (enter through npx create):

npx create-next-app@latest
npm install tailwindcss@next @tailwindcss/postcss@next
npm install next-themes

Global CSS (replace @tailwind lines for tailwind 4):

./src/app/global.css

@import "tailwindcss";
@variant dark (&:where([data-theme="dark"]));

./postcss.config.mjs

export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
};

./src/components/theme/ThemeSelect.jsx

'use client'

import { useTheme } from 'next-themes'

export default function ThemeSelect() {
  const { theme, setTheme } = useTheme();

  return (
    <select title='theme switcher'
      value={theme}
      onChange={(e) => setTheme(e.target.value)}
    >
      <option value="system">System</option>
      <option value="light">Light</option>
      <option value="dark">Dark</option>
    </select>
  );
}

Layout.tsx

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 attribute="data-theme" enableSystem>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

./src/app/page.jsx

import Image from "next/image";
import ThemeSelect from "../components/theme/ThemeSelect"

export default function Home() {
  return (
    <div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
      <main className="flex flex-col gap-8 row-start-2 items-center sm:items-start">
        <Image
          className="dark:invert"
          src="/next.svg"
          alt="Next.js logo"
          width={180}
          height={38}
          priority
        />

        <ThemeSelect />

        <ol className="list-inside list-decimal text-sm text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
          <li className="mb-2">
            Get started by editing{" "}
            <code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-semibold">
              src/app/page.tsx
            </code>
            .
          </li>
          <li className='text-black dark:text-green-400'>Save and see your changes instantly.</li>
        </ol>
      </main>
    </div>
  );
}

dark:invert and text-black dark:text-green-400 have no effect


Solution

  • Your variant configuration:

    @variant dark (&:where([data-theme="dark"]));
    

    Would only match if the element itself had data-theme="dark" on it, so dark: would only work like this:

    <div class="text-black dark:text-green-400" data-theme="dark">
      Green
      <span class="text-black dark:text-green-400">Black</span>
    </div>
    

    However, it seems like you'd like dark: to apply when a parent has data-theme="dark" on it. In which case, you should change your @variant rule to be:

    @variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
    

    The [data-theme="dark"] * candidate in the :where() matches if a parent has data-theme="dark":

    <div class="text-black dark:text-green-400" data-theme="dark">
      Green
      <span class="text-black dark:text-green-400">Green</span>
    </div>