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.