Overview

Tailwind v4 ships two dark mode strategies: class and media. Choose one per project, wire it at the root level, and let dark: variants do the rest. The parent reference for Tailwind configuration is tailwind; for the token system that powers theme switching, see tailwind-theme.

Pick class strategy for user-controlled dark mode

Class strategy toggles dark mode by adding or removing the dark class on the <html> element. Use this when users should be able to override the system preference.

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

Wire a small inline script in the document <head> to avoid a flash of wrong theme:

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

The script runs synchronously before paint. No framework event loop means no flash.

Pick media strategy for system-preference-only dark mode

Media strategy uses @media (prefers-color-scheme: dark) and needs no JavaScript toggle. Use this when simplicity matters more than user override.

@import "tailwindcss";
 
@variant dark (@media (prefers-color-scheme: dark));

dark: variants apply whenever the system reports dark preference. There is no way for users to override this without using an OS-level setting.

Define both light and dark token values in @theme

Keep light and dark colors together in the token file. Use a [data-theme] attribute pattern for the class strategy or a media query for the media strategy.

@theme {
  --color-surface: oklch(0.98 0 0);
  --color-ink: oklch(0.2 0 0);
  --color-brand: oklch(0.72 0.18 264);
}
 
.dark {
  --color-surface: oklch(0.12 0 0);
  --color-ink: oklch(0.92 0 0);
  --color-brand: oklch(0.78 0.18 264);
}

Tailwind utilities like bg-surface and text-ink read the token at render time. Dark mode is one block of token overrides, not a parallel set of dark:bg-* classes everywhere.

Use dark: variants only for values that do not have a token

After defining light and dark tokens, you rarely need dark: variants on individual utilities. Reserve dark: for values that cannot be tokenized: opacity adjustments, shadow changes, or hover-state differences between themes.

<!-- Token does the work; no dark: variant needed -->
<div class="bg-surface text-ink">
 
<!-- dark: variant for a case without a direct token -->
<div class="shadow-sm dark:shadow-none">

A codebase with dark: on every color utility is a sign that the token system is not doing its job.

Persist the user preference in localStorage

For class-based dark mode, persist the preference so it survives page reloads.

function setTheme(theme) {
  document.documentElement.classList.toggle("dark", theme === "dark");
  localStorage.setItem("theme", theme);
}
 
// Read system preference as default
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
setTheme(localStorage.getItem("theme") ?? (prefersDark ? "dark" : "light"));

Always read prefers-color-scheme as the fallback for first-time visitors. Do not default to light mode; that breaks the experience for users whose system is set to dark.

Set the theme-color meta tag dynamically

The theme-color meta tag controls the browser chrome color on mobile. Update it when the theme changes.

const meta = document.querySelector('meta[name="theme-color"]');
function syncThemeColor(dark) {
  if (meta) meta.setAttribute("content", dark ? "#0b0b0c" : "#fafaf9");
}

Or declare both in the HTML and use media attributes:

<meta name="theme-color" content="#fafaf9" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#0b0b0c" media="(prefers-color-scheme: dark)" />

The meta approach is simpler and avoids a JavaScript dependency for a cosmetic feature.

Verify contrast in both modes

Dark mode introduces a second set of contrast ratios to audit. Every foreground-on-background combination needs to clear 4.5:1 for body text and 3:1 for large text per WCAG AA. The oklch color space makes this predictable: lower lightness values step linearly, so a dark-mode text token at oklch(0.92 0 0) on a surface at oklch(0.12 0 0) is straightforward to check. For the accessibility requirements see accessibility.