21-05-2025
development
Theme Switcher in Next.js with TailwindCSS and Shadcn/ui
This post walks through implementing a dynamic color theme switcher in a Next.js app using Tailwind CSS. It allows users to toggle between custom color themes like Caffeine, VintagePaper and Notebook, and it respects light/dark modes. The setup includes custom CSS variables, a theme context provider, and JSON-based color configurations for full control over design tokens.
How I built a theme switcher in Next.js with TailwindCSS
I’ve always liked the idea of letting users personalize how things look. So I recently added theme switching to my personal project using TailwindCSS and Next.js. Here’s how I made it work.
The goal
Let users choose between different themes like “Caffeine” or “NeoBrutalism,” and apply their colors globally - supporting both light and dark modes.
The approach
Rather than sticking to Tailwind’s default darkMode: 'class', I needed full flexibility. I used:
- CSS variables for color tokens
- Themes object that maps each theme’s light and dark palettes
- React state to toggle themes
Defining Themes
I created a themes object where each theme has light and dark color tokens (using OKLCH for consistency and accessibility):
// theme-colors.ts
export const themes = {
Caffeine: {
light: {
background: "oklch(0.98 0 0)",
foreground: "oklch(0.24 0 0)",
// ...
},
dark: {
background: "oklch(0.18 0 0)",
foreground: "oklch(0.95 0 0)",
// ...
},
},
// more themes...
};
I also defined a type for the theme names:
// theme-types.ts
export type ThemeColors =
| "Perpetuity"
| "VintagePaper"
| "Notebook"
| "NeoBrutalism"
| "KodamaGrove"
| "Doom64"
| "Cyberpunk"
| "Caffeine";
Setting Up React State
I used context + state to store and update the selected theme:
const [themeColor, setThemeColor] = useState<ThemeColors>("Caffeine");
Applying Theme Styles
When the theme changes, I inject its CSS variables into :root using document.documentElement.style.setProperty(...). Here’s a simple utility:
function applyTheme(theme: Record<string, string>) {
Object.entries(theme).forEach(([key, value]) => {
document.documentElement.style.setProperty(`--${key}`, value);
});
}
This runs whenever themeColor or dark mode changes.
theme/theme-colors.json
This file contains the raw color tokens for each theme and mode (light/dark):
{
"slate": {
"light": {
"color-text": "#0f172a",
"color-bg": "#f1f5f9",
"color-muted": "#64748b"
},
"dark": {
"color-text": "#f1f5f9",
"color-bg": "#0f172a",
"color-muted": "#94a3b8"
}
},
"amber": {
"light": {
"color-text": "#78350f",
"color-bg": "#fef3c7",
"color-muted": "#d97706"
},
"dark": {
"color-text": "#fef3c7",
"color-bg": "#78350f",
"color-muted": "#fbbf24"
}
},
"emerald": {
"light": {
"color-text": "#064e3b",
"color-bg": "#d1fae5",
"color-muted": "#10b981"
},
"dark": {
"color-text": "#d1fae5",
"color-bg": "#064e3b",
"color-muted": "#34d399"
}
}
}
theme/theme-data-provider.tsx
This loads and applies the correct color variables to :root.
"use client";
import { useEffect } from "react";
import themes from "./theme-colors.json";
import { useThemeColor } from "./use-theme-color";
export function ThemeDataProvider() {
const { themeColor } = useThemeColor();
useEffect(() => {
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const mode = isDark ? "dark" : "light";
const theme = themes[themeColor][mode];
for (const [key, value] of Object.entries(theme)) {
document.documentElement.style.setProperty(`--${key}`, value);
}
}, [themeColor]);
return null;
}
components/theme-toggle.tsx
A simple theme switcher using a select dropdown:
"use client";
import { useThemeColor } from "@/theme/use-theme-color";
import { themes } from "@/theme/theme-list"; // An array like: ["slate", "amber", "emerald"]
export function ThemeToggle() {
const { themeColor, setThemeColor } = useThemeColor();
return (
<select
className="rounded border px-2 py-1"
value={themeColor}
onChange={(e) => setThemeColor(e.target.value)}
>
{themes.map((t) => (
<option key={t} value={t}>
{t.charAt(0).toUpperCase() + t.slice(1)}
</option>
))}
</select>
);
}
Tailwind classes like text-[color:var(--color-text)] and `bg-[color:var(—color-bg)] will now respond to the selected theme.
Wrap-Up
Theme switching can be aesthetic, accessible, and fun - without overengineering. Hope this gave you some ideas to try it yourself. If you’d like to see how the code looks like you can view the source code on GitHub.