Dark/Light Modes in Tailwind
Updated for Tailwind v4. The original version used
addBaseinsidetailwind.config.mjs(the v3 pattern). Migrating to v4, I moved it to CSS but kept my color config by loading it with@config— a very low-friction migration. The concept —CSS variables in:root/.dark— is the same; what changes is the how.
When working with multi-theme templates, keeping colors consistent across components is a challenge. After several experiences with inconsistencies, I found an easy and scalable solution: defining colors as CSS variables in :root and .dark, and exposing them as Tailwind utilities.
Colors and themes under control
In Tailwind v4 you can still use your JS config by loading it with @config, and centralize the palette as CSS variables in @layer base:
/* global.css */
@import "tailwindcss";
@config "../../tailwind.config.mjs";
@layer base {
:root {
--color-background: #fefefe;
--color-foreground: #0f0f0f;
}
.dark {
--color-background: #1d1e26;
--color-foreground: #e8eaf0;
}
}
In the config I keep darkMode: "class" and map each color to its variable, so Tailwind generates utilities like bg-background or text-foreground:
// tailwind.config.mjs
export default {
darkMode: "class",
theme: {
extend: {
colors: {
background: "var(--color-background)",
foreground: "var(--color-foreground)",
},
},
},
};
With this I centralize my palette in a single place and manage light and dark effortlessly. A single variable change updates everything instantly—no searching and replacing across a thousand files. Toggling themes is as simple as adding the .dark class on <html>.
Performance and maintenance
The v4 engine (Oxide, written in Rust) makes builds noticeably faster and detects classes automatically. And loading the config with @config lets you migrate from v3 without rewriting your whole palette: darkMode and the color mapping stay where they were, and the variables live in CSS. The result: fewer inconsistencies and less time wasted hunting down stray values.
Conclusion
CSS variables in :root/.dark + the config loaded with @config give me consistency, performance, and dynamic themes with an almost painless v4 migration. It simplifies maintenance and keeps templates consistent. If you care about maintainable code and a consistent UX, it’s a solid default worth adopting.