Skip to main content
Back to Blog
Tutorials
3 min read
November 16, 2024

How to Create a Theme Switcher with Multiple Themes in React

Build a theme switcher that goes beyond dark mode with multiple color themes using CSS custom properties and React context.

Ryel Banfield

Founder & Lead Developer

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.

themesCSS variablesReactcustomizationtutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles