Skip to content

Tailwind Dark Mode

Tailwind CSS has built-in support for dark mode through the dark: variant. This lesson covers both the automatic media-based strategy and the manual class-based strategy, including a working toggle button.

What Is Dark Mode in Tailwind?

Dark mode lets you define an alternate color scheme that activates automatically based on the user's operating system setting, or manually through a toggle in your UI. Tailwind supports both approaches through the same dark: variant prefix.

You style the light-mode appearance normally, then add dark: prefixed utilities to override colors, backgrounds, and borders specifically for dark mode, all on the same element.

<div class="bg-white text-slate-900 dark:bg-slate-900 dark:text-white p-6 rounded-lg">
  <h2 class="font-bold">Card Title</h2>
  <p class="text-slate-600 dark:text-slate-300">Card description text.</p>
</div>

In light mode the card is white with dark text; in dark mode the background and text colors flip automatically.

Dark Mode Strategy Configuration

// tailwind.config.js
export default {
  darkMode: "media",  // follows OS preference (default)
  // or
  darkMode: "class",  // toggled by a "dark" class on <html>
};
  • media strategy uses the prefers-color-scheme CSS media query and requires no JavaScript.
  • class strategy activates dark styles only when a dark class is present on an ancestor, usually <html>.
  • The class strategy is required if you want a manual toggle button controlled by JavaScript.
  • In Tailwind v4, dark mode can also be configured with the @custom-variant directive in CSS.

Dark Mode Cheatsheet

Common dark-mode utility pairs and configuration options.

Purpose Example Notes
Background flip bg-white dark:bg-slate-900 Most common dark mode pattern
Text color flip text-slate-900 dark:text-slate-100 Keeps text readable in both modes
Border flip border-slate-200 dark:border-slate-700 Softer border contrast in dark mode
Muted text flip text-slate-500 dark:text-slate-400 Secondary text stays legible
Shadow adjustment shadow-md dark:shadow-none Shadows often look wrong on dark backgrounds
Image dimming dark:brightness-90 Slightly dims images in dark mode
Config: media darkMode: "media" Automatic, follows OS setting
Config: class darkMode: "class" Manual, toggled via JS
Toggle target document.documentElement.classList.toggle("dark") Adds/removes the dark class
Persist choice localStorage.setItem("theme", "dark") Remembers user preference

The Media Strategy (Automatic)

With darkMode: "media" (the default in Tailwind v3), dark styles apply automatically whenever the user's operating system is set to dark mode, using the prefers-color-scheme: dark media query. No JavaScript or toggle button is needed.

<!-- tailwind.config.js: darkMode: "media" -->
<body class="bg-white text-black dark:bg-black dark:text-white">
  Automatically follows the OS theme.
</body>

The Class Strategy With a Manual Toggle

With darkMode: "class", dark styles only apply when a dark class exists on an ancestor element, usually <html>. This lets users manually switch themes with a button, independent of their OS setting.

<button id="theme-toggle" class="rounded-md border px-3 py-1.5 dark:border-slate-600">
  Toggle Theme
</button>

<script>
  const root = document.documentElement;
  const stored = localStorage.getItem("theme");
  if (stored === "dark") root.classList.add("dark");

  document.getElementById("theme-toggle").addEventListener("click", () => {
    root.classList.toggle("dark");
    localStorage.setItem("theme", root.classList.contains("dark") ? "dark" : "light");
  });
</script>

The script checks localStorage on load so the chosen theme persists across page visits, then toggles the dark class and saves the new preference on click.

Avoiding a Flash of the Wrong Theme

If the theme-detection script runs after the page renders, users can briefly see the wrong theme flash before it switches. Place a small inline script in the document head, before any content renders, to apply the dark class immediately.

<head>
  <script>
    if (
      localStorage.getItem("theme") === "dark" ||
      (!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)
    ) {
      document.documentElement.classList.add("dark");
    }
  </script>
</head>

Accessible Dark Mode Toggles

A dark mode toggle should be a real <button>, announce its current state, and maintain sufficient contrast ratios in both themes, not just visually pleasing colors.

<button
  type="button"
  aria-pressed="false"
  aria-label="Toggle dark mode"
  class="rounded-md border px-3 py-1.5"
>
  🌙
</button>
  • Use aria-pressed to communicate the toggle's current on/off state to screen readers.
  • Verify text and background color combinations meet WCAG AA contrast in both light and dark themes.
  • Respect the user's OS preference by default; only override it after an explicit manual choice.

Common Mistakes

  • Forgetting to set darkMode: "class" in the config, so a manual JavaScript toggle has no effect.
  • Only styling backgrounds for dark mode and forgetting text, border, and shadow colors, which breaks contrast.
  • Not persisting the user's theme choice, causing it to reset on every page reload.
  • Loading the theme-detection script after page content renders, causing a visible flash of the wrong theme.
  • Assuming dark mode is just inverted colors; some colors (like brand colors) may need entirely different dark-mode shades to stay legible.

Key Takeaways

  • Tailwind's dark: variant applies utilities only in dark mode, using either the media or class strategy.
  • The media strategy follows the OS setting automatically with no JavaScript needed.
  • The class strategy enables a manual toggle by adding or removing a dark class on <html>.
  • Persist the user's theme choice in localStorage and apply it before the page paints to avoid a flash.
  • Always update text, border, and shadow colors alongside backgrounds when switching themes.

Pro Tip

Store the user's preference as "light", "dark", or absent (meaning "follow system"), and check prefers-color-scheme only when no explicit choice has been saved yet.