Dark/Light Modes in Tailwind

Image of dark/light themes in Tailwind
Image of dark/light themes in Tailwind

Updated for Tailwind v4. The original version used addBase inside tailwind.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.