Go beyond light/dark mode. Here is how to build a theme system that supports multiple color themes.
Step 1: Define Themes with CSS Variables
/* globals.css */
:root {
--bg-primary: #ffffff;
--bg-secondary: #f9fafb;
--text-primary: #111827;
--text-secondary: #6b7280;
--accent: #3b82f6;
--accent-hover: #2563eb;
--border: #e5e7eb;
--card-bg: #ffffff;
}
[data-theme="dark"] {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--accent: #60a5fa;
--accent-hover: #93bbfd;
--border: #334155;
--card-bg: #1e293b;
}
[data-theme="ocean"] {
--bg-primary: #0c1222;
--bg-secondary: #0f1b2d;
--text-primary: #e0f2fe;
--text-secondary: #7dd3fc;
--accent: #0ea5e9;
--accent-hover: #38bdf8;
--border: #1e3a5f;
--card-bg: #0f1b2d;
}
[data-theme="forest"] {
--bg-primary: #0f1f0f;
--bg-secondary: #162616;
--text-primary: #dcfce7;
--text-secondary: #86efac;
--accent: #22c55e;
--accent-hover: #4ade80;
--border: #1a3a1a;
--card-bg: #162616;
}
[data-theme="sunset"] {
--bg-primary: #1c0f0a;
--bg-secondary: #2a1810;
--text-primary: #fef3c7;
--text-secondary: #fbbf24;
--accent: #f59e0b;
--accent-hover: #fbbf24;
--border: #3d2a1a;
--card-bg: #2a1810;
}
[data-theme="lavender"] {
--bg-primary: #faf5ff;
--bg-secondary: #f3e8ff;
--text-primary: #581c87;
--text-secondary: #7e22ce;
--accent: #a855f7;
--accent-hover: #9333ea;
--border: #e9d5ff;
--card-bg: #faf5ff;
}
Step 2: Theme Context
"use client";
import { createContext, useContext, useEffect, useState } from "react";
type Theme = "light" | "dark" | "ocean" | "forest" | "sunset" | "lavender";
interface ThemeContextType {
theme: Theme;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [theme, setTheme] = useState<Theme>("light");
useEffect(() => {
const saved = localStorage.getItem("theme") as Theme | null;
if (saved) setTheme(saved);
}, []);
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
localStorage.setItem("theme", theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) throw new Error("useTheme must be used within ThemeProvider");
return context;
}
Step 3: Theme Switcher Component
"use client";
import { useTheme } from "./ThemeProvider";
import { Check } from "lucide-react";
const themes = [
{ id: "light" as const, name: "Light", colors: ["#ffffff", "#3b82f6"] },
{ id: "dark" as const, name: "Dark", colors: ["#0f172a", "#60a5fa"] },
{ id: "ocean" as const, name: "Ocean", colors: ["#0c1222", "#0ea5e9"] },
{ id: "forest" as const, name: "Forest", colors: ["#0f1f0f", "#22c55e"] },
{ id: "sunset" as const, name: "Sunset", colors: ["#1c0f0a", "#f59e0b"] },
{ id: "lavender" as const, name: "Lavender", colors: ["#faf5ff", "#a855f7"] },
];
export function ThemeSwitcher() {
const { theme, setTheme } = useTheme();
return (
<div className="space-y-3">
<h3 className="text-sm font-medium" style={{ color: "var(--text-primary)" }}>
Choose Theme
</h3>
<div className="grid grid-cols-3 gap-2">
{themes.map((t) => (
<button
key={t.id}
onClick={() => setTheme(t.id)}
className="relative flex flex-col items-center gap-1.5 rounded-lg border p-3 transition-all hover:scale-105"
style={{
borderColor: theme === t.id ? "var(--accent)" : "var(--border)",
backgroundColor: "var(--card-bg)",
}}
>
<div className="flex gap-1">
{t.colors.map((color, i) => (
<div
key={i}
className="h-6 w-6 rounded-full border border-white/20"
style={{ backgroundColor: color }}
/>
))}
</div>
<span
className="text-xs"
style={{ color: "var(--text-secondary)" }}
>
{t.name}
</span>
{theme === t.id && (
<div
className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full"
style={{ backgroundColor: "var(--accent)" }}
>
<Check className="h-3 w-3 text-white" />
</div>
)}
</button>
))}
</div>
</div>
);
}
Step 4: Using Theme Variables in Components
export function ThemedCard({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
<div
className="rounded-xl border p-6"
style={{
backgroundColor: "var(--card-bg)",
borderColor: "var(--border)",
}}
>
<h2
className="text-lg font-bold"
style={{ color: "var(--text-primary)" }}
>
{title}
</h2>
<div
className="mt-2 text-sm"
style={{ color: "var(--text-secondary)" }}
>
{children}
</div>
</div>
);
}
export function ThemedButton({
children,
onClick,
}: {
children: React.ReactNode;
onClick?: () => void;
}) {
return (
<button
onClick={onClick}
className="rounded-lg px-4 py-2 text-sm font-medium text-white transition-colors"
style={{ backgroundColor: "var(--accent)" }}
onMouseEnter={(e) =>
((e.target as HTMLElement).style.backgroundColor = "var(--accent-hover)")
}
onMouseLeave={(e) =>
((e.target as HTMLElement).style.backgroundColor = "var(--accent)")
}
>
{children}
</button>
);
}
Step 5: Tailwind Integration
// tailwind.config.ts (for Tailwind v3)
export default {
theme: {
extend: {
colors: {
theme: {
bg: "var(--bg-primary)",
"bg-secondary": "var(--bg-secondary)",
text: "var(--text-primary)",
"text-secondary": "var(--text-secondary)",
accent: "var(--accent)",
"accent-hover": "var(--accent-hover)",
border: "var(--border)",
card: "var(--card-bg)",
},
},
},
},
};
// Now use in classes
<div className="bg-theme-bg text-theme-text border-theme-border">
<h1 className="text-theme-text">Hello</h1>
<button className="bg-theme-accent hover:bg-theme-accent-hover">
Click
</button>
</div>
Need a Custom Theming System?
We build web applications with customizable themes, brand-consistent design systems, and white-label solutions. Contact us to discuss your project.