In Tailwind 3 I set darkMode: 'class' in tailwind.config.js. That key is gone in v4 and dark: only follows my OS setting. How do I make a manual toggle work?
#tailwind#tailwind-4#dark-mode
DEV3 Developer Forum
Every post is reviewed first. Usually reviewed within 24 hours.
In Tailwind 3 I set darkMode: 'class' in tailwind.config.js. That key is gone in v4 and dark: only follows my OS setting. How do I make a manual toggle work?
#tailwind#tailwind-4#dark-mode
Default behavior
In Tailwind 4 dark: uses the prefers-color-scheme media query. To control it with a class or attribute you override the variant in CSS.
Class strategy
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
Now dark:bg-slate-900 applies when an ancestor (typically <html>) has the class dark.
Data attribute strategy
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
Toggle script with persistence (avoid a flash)
Run this as a small inline script in the head, before the body renders (it must be synchronous so the first paint is correct):
try {
const stored = localStorage.getItem('theme');
const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.toggle('dark', stored ? stored === 'dark' : prefersDark);
} catch (e) {}
Toggle button:
function toggleTheme() {
const dark = document.documentElement.classList.toggle('dark');
try { localStorage.setItem('theme', dark ? 'dark' : 'light'); } catch (e) {}
}
In Next.js use next-themes with attribute="class" and add suppressHydrationWarning on <html>.
Style with variables for less repetition
:root { --bg: white; --fg: #111; }
.dark { --bg: #0b0b0f; --fg: #eee; }
@theme inline {
--color-surface: var(--bg);
--color-ink: var(--fg);
}
Then bg-surface text-ink works in both themes without dark: on every element.
Pitfalls
color-scheme: dark on .dark so form controls and scrollbars match.darkMode from the JS config, it is ignored.