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

Free tools for this guide

Skip the manual work. These run in your browser and give you copy-paste Tailwind CSS. See all 34 tools

Keep reading