How to Apply Multiple Themes in Next.js (Blue, Green, Red and More) Without Refreshing or Using `dark:` Classes
Your site has a dark mode. Then a client asks, "Can we also have a blue theme? And a green one? Oh, and red!" 😅 Suddenly you are staring at dark:bg-gray-900 blue:bg-blue-900 green:bg-green-900 red:bg-red-900 on every el
Your site has a dark mode. Then a client asks, "Can we also have a blue theme? And a green one? Oh, and red!" 😅
Suddenly you are staring at dark:bg-gray-900 blue:bg-blue-900 green:bg-green-900 red:bg-red-900 on every element. That is a lot of repeated classes.
There is a cleaner way. You define every theme's colors once in your global CSS. To switch themes, you change one class on the <html> element. No page refresh, no HTML changes, no per-element theme classes.
Ready to see how it works? 🚀
What Is a Multi-Theme System in Next.js?
A multi-theme system lets users switch between more than two color schemes, such as light, dark, blue, green, and red. The switch happens instantly, with no reload.
Think of a TV remote. The TV (your HTML) stays exactly the same. You press a button (change one class) and the colors change.
The idea has three parts:
-
CSS variables hold the colors, like
--backgroundand--primary. -
A class on
<html>, likeblueorred, decides which set of values is active. -
Your components use names like
bg-backgroundandtext-foreground, never specific colors.
Your components never know which theme is active. They just ask for "the background color" and the CSS answers.
Why This Topic Matters
Theming gets messy fast when colors are written directly into components.
- Adding a new theme means editing many files.
- Rebranding means hunting through the whole codebase for color classes.
- Markup gets longer and harder to read.
With the variable approach, a new theme is one new block in one CSS file. Components stay untouched.
Benefits with Real-Life Examples
Clean HTML and JSX.
className="bg-background text-foreground"replaces a long chain of per-theme classes. A card component looks the same in every theme.One place for all colors.
Your client says, "Make the green theme a bit softer." You change one value inglobals.css, and every button, card, and navbar follows.Easy to add new themes.
Want a purple theme? Add a.purple { ... }block. No component changes.Instant switching, no refresh.
Changing a class on<html>makes the browser re-apply styles. There is no page reload, and React does not need to re-render your components just to change colors.Less repeated code.
You are not generating and shipping theme-specific variants for every element, which keeps your markup and CSS leaner. (It will not magically shrink your app by half, but the duplication really does go away.)
Step-by-Step: Build It
This guide assumes the Next.js App Router and Tailwind CSS v4. A note for Tailwind v3 users appears later.
Step 1: Define Your Themes in globals.css
@import "tailwindcss";
/* Default (light) theme */
:root {
--background: #ffffff;
--foreground: #111827;
--card: #f3f4f6;
--primary: #4f46e5;
--primary-foreground: #ffffff;
color-scheme: light;
}
.dark {
--background: #0b0f19;
--foreground: #e5e7eb;
--card: #151b2b;
--primary: #818cf8;
--primary-foreground: #0b0f19;
color-scheme: dark;
}
.blue {
--background: #eff6ff;
--foreground: #1e3a8a;
--card: #dbeafe;
--primary: #2563eb;
--primary-foreground: #ffffff;
}
.green {
--background: #f0fdf4;
--foreground: #14532d;
--card: #dcfce7;
--primary: #16a34a;
--primary-foreground: #ffffff;
}
.red {
--background: #fef2f2;
--foreground: #7f1d1d;
--card: #fee2e2;
--primary: #dc2626;
--primary-foreground: #ffffff;
}
Every theme defines the same variable names with different values. That is the whole trick. 💡
Step 2: Connect the Variables to Tailwind
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
}
Now bg-background, text-foreground, bg-card, and bg-primary work as normal Tailwind classes. They read the live CSS variable, so they follow whichever theme class is active.
Step 3: Use Semantic Classes in Your Components
export function PricingCard() {
return (
<div className="rounded-xl bg-card p-6 text-foreground">
<h3 className="text-xl font-bold">Pro Plan</h3>
<p>Everything you need to ship faster.</p>
<button className="mt-4 rounded-lg bg-primary px-4 py-2 text-primary-foreground">
Get Started
</button>
</div>
);
}
There are no dark:, blue:, or red: classes anywhere. This markup works for every theme you will ever add.
Step 4: Create the Theme Switcher
// components/theme-switcher.tsx
"use client";
const THEMES = ["light", "dark", "blue", "green", "red"] as const;
type Theme = (typeof THEMES)[number];
export function ThemeSwitcher() {
function applyTheme(theme: Theme) {
const root = document.documentElement;
root.classList.remove(...THEMES);
root.classList.add(theme);
localStorage.setItem("theme", theme);
}
return (
<div className="flex gap-2">
{THEMES.map((theme) => (
<button
key={theme}
onClick={() => applyTheme(theme)}
className="rounded-md bg-primary px-3 py-1 text-sm capitalize text-primary-foreground"
>
{theme}
</button>
))}
</div>
);
}
Clicking a button removes the old theme class and adds the new one. The colors change instantly. The choice is saved in localStorage so it survives a reload.
Step 5: Avoid the "Flash of Wrong Theme"
You have probably seen this: the page loads white, then flips to dark a moment later. It happens because the saved theme is applied only after React loads.
The fix is a tiny inline script that runs before the page paints:
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
__html: `
try {
var t = localStorage.getItem('theme');
if (t) document.documentElement.classList.add(t);
} catch (e) {}
`,
}}
/>
</head>
<body className="bg-background text-foreground">{children}</body>
</html>
);
}
suppressHydrationWarning on <html> is needed because the script changes the class before React hydrates. Without it, React may warn about a mismatch.
Comparison: Per-Element Theme Classes vs CSS Variables
Per-element classes (dark:, red:) |
CSS variables + one class | |
|---|---|---|
| Markup length | Grows with every theme | Stays the same |
| Adding a new theme | Edit many components | Add one CSS block |
| Changing a color | Search across files | Edit one value |
| Best for | Quick one-off tweaks | Real multi-theme apps |
Per-element classes are not "wrong". For a simple light/dark site they work fine. But once you go beyond two themes, variables win on maintainability.
Best Tips and Do's & Don'ts
Do ✅
- Use semantic names (
background,primary,card) instead of color names (blue-500). - Define the same set of variables in every theme.
- Set
color-schemefor dark themes so native elements like scrollbars and form inputs match. - Test text contrast in every theme. A readable blue theme can still hide text in the red one.
Don't ❌
- Don't put theme-specific colors directly in components.
- Don't forget the pre-paint script, or users will see a flash.
- Don't skip
suppressHydrationWarningon<html>if you use that script.
Tailwind v3 note: v3 has no @theme block. Map the variables in tailwind.config.js instead, for example background: "var(--background)". If you want opacity modifiers like bg-primary/50 in v3, store the variables as RGB channels (like 253 110 78) and use rgb(var(--primary) / <alpha-value>).
Prefer a library? next-themes supports custom theme names through its themes prop and handles storage and the flash problem for you. The CSS-variable idea in this article works the same way with it.
Common Mistakes People Make
Using
dark:plus custom variants for every theme.
It feels natural because that is how dark mode is usually taught. The fix is to define semantic variables once.Forgetting to remove the old class.
If you only addredand never removeblue, both classes stay on<html>, and CSS order decides who wins. Always remove the old theme class first.Missing variables in one theme.
If.greenforgets--card, it inherits the value from:root, so you get odd colors. Keep every theme's variable list identical.Reading
localStorageduring server rendering.
localStorageonly exists in the browser. Use it inside event handlers, effects, or the inline script, not in server components.Ignoring accessibility.
A pretty theme with low contrast is a bad theme. Check readability for every one.
Conclusion
Here is the recipe again:
- Define each theme's colors once in
globals.cssusing CSS variables. - Map those variables to semantic Tailwind colors.
- Use classes like
bg-backgroundandtext-primaryin your components. - Switch themes by changing one class on
<html>. - Add a tiny pre-paint script so users never see a flash.
The result is cleaner JSX, a single place to manage colors, and instant theme switching with no refresh. Adding a new theme becomes a ten-line job. 😊
If this helped, you can read more practical dev guides at hamidrazadev.com. Share it with a developer friend who is still typing dark: on every div 😄, and tell me in the comments which theme you would add first.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.