// Leopold Jurić
Open navigation menu

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.