Guide · 11 min read

Tailwind CSS Dark Mode: Complete Setup Guide 2026

Dark mode is now a user expectation. Tailwind CSS makes implementing dark mode straightforward with its built-in dark: prefix. This guide covers both configuration strategies, toggling with React/Next.js, and converting components to support dark mode.

Dark Mode Strategies: Class vs Media

Tailwind CSS supports two dark mode strategies. You configure this in tailwind.config.ts:

Class Strategy (recommended)

darkMode: 'class'

Dark mode activates when the dark class is on the <html> element. You control it with JavaScript.

  • ✓ User can manually toggle light/dark
  • ✓ Can persist preference in localStorage
  • ✓ Can default to system preference + allow override

Media Strategy

darkMode: 'media'

Dark mode follows the OS setting (prefers-color-scheme: dark) automatically. No JavaScript needed.

  • ✓ Zero JavaScript required
  • ✗ No manual toggle possible
  • ✗ Users can't override OS setting in-app

Recommendation: Use class strategy. It gives you full control and lets users override their OS setting — which is the behavior most apps (VS Code, GitHub, Notion) provide.

Setting Up Tailwind CSS Dark Mode

In Tailwind CSS v3, add darkMode to your config:

// tailwind.config.ts (v3)
import type { Config } from "tailwindcss";

export default {
  darkMode: "class",
  content: ["./src/**/*.{ts,tsx}"],
  theme: { extend: {} },
  plugins: [],
} satisfies Config;

In Tailwind CSS v4, configure dark mode in your CSS file:

/* globals.css (v4) */
@import "tailwindcss";

@variant dark (&:where(.dark, .dark *));

Using the dark: Prefix

Once configured, apply dark mode styles by prefixing any Tailwind utility class with dark:. The class only applies when the dark class is active on the <html> element.

{/* A card that adapts to dark mode */}
<div className="bg-white dark:bg-gray-800
  border border-gray-200 dark:border-gray-700
  rounded-2xl p-6 shadow-sm dark:shadow-gray-900/50">
  
  <h2 className="text-xl font-bold text-gray-900 dark:text-white">
    Card Title
  </h2>
  
  <p className="text-gray-600 dark:text-gray-400 mt-2">
    Card description text that adapts to dark mode.
  </p>
  
  <button className="mt-4 px-4 py-2 bg-cyan-700 dark:bg-cyan-600
    text-white rounded-lg hover:bg-cyan-800 dark:hover:bg-cyan-500">
    Action
  </button>
</div>

Essential Dark Mode Utility Classes

dark:bg-gray-900Dark background (near black)
dark:bg-gray-800Slightly lighter dark background
dark:text-whiteWhite text in dark mode
dark:text-gray-300Muted text in dark mode
dark:border-gray-700Dark border color
dark:border-gray-600Slightly lighter dark border
dark:shadow-gray-900/50Dark shadow with opacity
dark:placeholder:text-gray-500Placeholder in dark mode
dark:hover:bg-gray-700Hover state in dark mode
dark:focus:ring-cyan-400Focus ring color in dark mode

Dark Mode Toggle in React / Next.js

Here is a simple dark mode toggle that persists user preference in localStorage:

"use client";
import { useEffect, useState } from "react";

export function DarkModeToggle() {
  const [dark, setDark] = useState(false);

  useEffect(() => {
    // Load from localStorage on mount
    const stored = localStorage.getItem("theme");
    const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
    const isDark = stored === "dark" || (!stored && prefersDark);
    
    setDark(isDark);
    document.documentElement.classList.toggle("dark", isDark);
  }, []);

  const toggle = () => {
    const next = !dark;
    setDark(next);
    document.documentElement.classList.toggle("dark", next);
    localStorage.setItem("theme", next ? "dark" : "light");
  };

  return (
    <button
      onClick={toggle}
      className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
      aria-label="Toggle dark mode"
    >
      {dark ? (
        <svg className="w-5 h-5 text-yellow-400" fill="currentColor" viewBox="0 0 20 20">
          <path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1z..." />
        </svg>
      ) : (
        <svg className="w-5 h-5 text-gray-600" fill="currentColor" viewBox="0 0 20 20">
          <path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
        </svg>
      )}
    </button>
  );
}

Using next-themes for System Preference + Manual Toggle

For production Next.js apps, next-themes is the recommended solution. It handles SSR flash prevention, system preference detection, localStorage persistence, and multi-tab syncing:

# Install
npm install next-themes

// app/layout.tsx
import { ThemeProvider } from "next-themes";

export default function RootLayout({ children }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

// components/DarkModeToggle.tsx
"use client";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

export function DarkModeToggle() {
  const { theme, setTheme } = useTheme();
  const [mounted, setMounted] = useState(false);
  
  // Avoid hydration mismatch
  useEffect(() => setMounted(true), []);
  if (!mounted) return null;

  return (
    <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
      {theme === "dark" ? "☀️ Light" : "🌙 Dark"}
    </button>
  );
}

Dark Mode Component Examples

Converting common components to support dark mode:

Navbar

bg-white border-b border-gray-200dark:bg-gray-900 dark:border-gray-800text-gray-700 hover:text-gray-900dark:text-gray-300 dark:hover:text-white

Card

bg-white border border-gray-200 shadow-smdark:bg-gray-800 dark:border-gray-700 dark:shadow-gray-900/50text-gray-900dark:text-white

Button (Primary)

bg-cyan-700 text-white hover:bg-cyan-800dark:bg-cyan-600 dark:hover:bg-cyan-500

Input

bg-white border border-gray-300 text-gray-900dark:bg-gray-800 dark:border-gray-600 dark:text-whiteplaceholder:text-gray-400dark:placeholder:text-gray-500

Page Background

bg-whitedark:bg-gray-950text-gray-900dark:text-gray-100

Frequently Asked Questions

How do I prevent the dark mode flash on page load in Next.js?

Use the next-themes library with suppressHydrationWarning on the <html> element. Alternatively, inject a blocking script in <head> that reads localStorage and applies the dark class before React hydrates.

Can I use dark mode with Tailwind CSS v4?

Yes. In Tailwind v4, configure dark mode with @variant dark (&:where(.dark, .dark *)) in your CSS file. The dark: prefix works identically to v3.

Should I store dark mode preference in localStorage or cookies?

localStorage is fine for most cases. Use cookies if you do server-side rendering and need the preference available on the server (to avoid a flash). next-themes handles both approaches.

How do I apply dark mode to images in Tailwind?

For decorative images, use dark:opacity-80 to slightly dim them. For images that should change completely in dark mode (like logos), use dark:hidden on the light version and hidden dark:block on the dark version.

Why is my Tailwind dark mode not working?

Common causes: (1) darkMode is not set to 'class' in tailwind.config. (2) The 'dark' class is not being applied to <html>. (3) The class is applied but Tailwind hasn't purged the dark: variants. (4) Browser cache — hard refresh with Ctrl+Shift+R.

Browse Free Tailwind CSS Components

All components include Tailwind CSS code you can adapt for dark mode.

Related Guides