shadcn-theme-switcher
v0.1.0
Published
A flexible theme switcher component for shadcn/ui with support for light/dark mode and multiple color themes
Maintainers
Readme
shadcn-theme-switcher
A flexible, production-ready theme switcher for shadcn/ui with support for multiple themes options with light/dark theme mode.
Usecases
Perfect for:
- 🎯 User Personalization - Allow your users to personalize their experience with multiple theme options.
- 🧪 Design Exploration - Quickly prototype and compare different themes before committing to a final one.
- 🏢 Multi-Brand Apps - Support different brands or clients within a single application by customizing to their color schemes dynamically.
- ♿ Accessibility - Provide theme variations optimized for different visual preferences and needs.
Features
- 🎨 Multiple Color Themes - Switch between different color schemes seamlessly
- 🌓 Light/Dark Mode - Support for light, dark, and system preference modes
- 💾 Persistent Storage - Themes and modes persist across sessions using localStorage
- 🔄 Cross-Tab Sync - Theme changes sync automatically across browser tabs
- 🎭 Custom Themes - Easily create and use your own custom themes
- 🔤 Google Fonts - Automatic font loading for themed typography
- ⚡ Zero Config - Works out of the box with sensible defaults
- 🪝 Headless Hooks - Use the hooks directly for custom implementations
- 📦 Tree-Shakeable - Only import what you need
Installation
npm install shadcn-theme-switcherPeer Dependencies
This package requires the following peer dependencies (which should already be installed in your shadcn/ui project):
npm install class-variance-authority clsx tailwind-merge tailwindcssQuick Start
Just add the Theme Switcher components and provide the themes.
import { ThemeSwitcher, ModeSwitcher } from "shadcn-theme-switcher";
import { defaultThemes } from "shadcn-theme-switcher/themes";
function App() {
return (
<nav>
<ThemeSwitcher themes={defaultThemes} defaultTheme="default" />
<ModeSwitcher defaultMode="system" />
</nav>
);
}That's it! Your app now has theme switching capabilities.
Components
ModeSwitcher
A dropdown component for switching between light, dark, and system modes.
Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| defaultMode | "light" \| "dark" \| "system" | "system" | Initial mode when no preference is stored |
| className | string | undefined | Additional CSS classes |
Example
import { ModeSwitcher } from "shadcn-theme-switcher";
function Navigation() {
return <ModeSwitcher defaultMode="dark" className="w-32" />;
}ThemeSwitcher
A dropdown component for switching between different color themes with theme palette previews.
Props
| Prop | Type | Required | Description |
|------|------|----------|-------------|
| themes | ThemeConfig[] | Yes | Array of theme configurations |
| defaultTheme | string | No | Initial theme name (uses first theme if not provided) |
| className | string | No | Additional CSS classes |
Example
import { ThemeSwitcher } from "shadcn-theme-switcher";
import { defaultThemes } from "shadcn-theme-switcher/themes";
function Navigation() {
return (
<ThemeSwitcher
themes={defaultThemes}
defaultTheme="violet-bloom"
className="min-w-[200px]"
/>
);
}Hooks
For more control or custom implementations, use the hooks directly.
useThemeMode
Manages light/dark mode state and system preference detection.
Returns
| Property | Type | Description |
|----------|------|-------------|
| mode | "light" \| "dark" \| "system" | Current mode |
| setMode | (mode: ThemeMode) => void | Function to change mode |
| effectiveMode | "light" \| "dark" | Resolved mode (system → light/dark) |
Example
import { useThemeMode } from "shadcn-theme-switcher";
function CustomModeSwitcher() {
const { mode, setMode, effectiveMode } = useThemeMode({
defaultMode: "system"
});
return (
<div>
<p>Current Mode: {mode}</p>
<p>Effective Mode: {effectiveMode}</p>
<button onClick={() => setMode("light")}>Light</button>
<button onClick={() => setMode("dark")}>Dark</button>
<button onClick={() => setMode("system")}>System</button>
</div>
);
}useTheme
Manages color theme state and persistence.
Returns
| Property | Type | Description |
|----------|------|-------------|
| theme | string | Current theme name |
| setTheme | (theme: string) => void | Function to change theme |
Note: You have to handle the font loading yourself, use
applyThemeFonts
Example
import {useEffect} from "react"
import { useTheme, applyThemeFonts } from "shadcn-theme-switcher";
import { defaultThemes } from "shadcn-theme-switcher/themes";
function CustomThemeSwitcher() {
const { theme, setTheme } = useTheme({ defaultTheme: "default" });
const currentTheme = defaultThemes.find((t) => t.name === theme);
useEffect(() => {
const fonts = currentTheme?.fonts;
if (fonts) applyThemeFonts(fonts);
}, [currentTheme]);
return (
<div>
<p>Current Theme: {theme}</p>
<button onClick={() => setTheme("violet-bloom")}>Violet Bloom</button>
<button onClick={() => setTheme("mocha-mousse")}>Mocha Mousse</button>
</div>
);
}Default Themes
The package includes 15 pre-built themes:
| Theme Name | Description |
|------------|-------------|
| default | The standard shadcn/ui theme |
| modern-minimal | Clean and modern minimalist design |
| violet-bloom | Vibrant violet and purple palette |
| t3-chat | Chat-inspired warm tones |
| twitter | Classic Twitter blue theme |
| mocha-mousse | Warm coffee-inspired browns |
| bubblegum | Playful pink and pastel colors |
| amethyst-haze | Mystical purple haze |
| graphite | Sleek graphite gray tones |
| cosmic-night | Deep cosmic purple palette |
| mono | Pure monochrome design |
| notebook | Paper-like notebook aesthetic |
| doom-64 | Retro gaming-inspired colors |
| catppuccin | Pastel soothing color scheme |
| perpetuity | Terminal-style teal theme |
| tangerine | Warm tangerine orange accents |
Import them with:
import { defaultThemes } from "shadcn-theme-switcher/themes";Creating Custom Themes
Step 1: Define CSS Variables
Create a CSS file with your theme's color variables using OKLCH format:
/* custom-themes.css */
/* Common settings for both light and dark modes */
:root[data-theme="forest-green"],
[data-theme="forest-green"] {
/* Optional: Custom fonts */
--font-sans: Poppins, sans-serif;
--font-mono: Fira Code, monospace;
--font-serif: Georgia, serif;
/* Border radius */
--radius: 0.5rem;
/* Optional: Letter spacing adjustments */
--tracking-normal: 0em;
}
/* Light mode colors */
:root[data-theme="forest-green"]:not(.dark),
/* This extra selector applies to non root elements such as theme palette preview */
[data-theme="forest-green"]:not(.dark) {
--background: oklch(1 0 0);
--foreground: oklch(0.3 0.08 145);
--card: oklch(1 0 0);
--card-foreground: oklch(0.3 0.08 145);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.3 0.08 145);
--primary: oklch(0.5 0.15 145);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.95 0.02 145);
--secondary-foreground: oklch(0.3 0.08 145);
--muted: oklch(0.97 0.01 145);
--muted-foreground: oklch(0.5 0.03 145);
--accent: oklch(0.92 0.04 145);
--accent-foreground: oklch(0.35 0.1 145);
--destructive: oklch(0.6 0.2 25);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.92 0.02 145);
--input: oklch(0.92 0.02 145);
--ring: oklch(0.5 0.15 145);
}
/* Dark mode colors */
:root[data-theme="forest-green"].dark,
[data-theme="forest-green"].dark {
--background: oklch(0.2 0.02 145);
--foreground: oklch(0.95 0.01 145);
--card: oklch(0.25 0.02 145);
--card-foreground: oklch(0.95 0.01 145);
--popover: oklch(0.25 0.02 145);
--popover-foreground: oklch(0.95 0.01 145);
--primary: oklch(0.6 0.15 145);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(0.3 0.03 145);
--secondary-foreground: oklch(0.95 0.01 145);
--muted: oklch(0.3 0.03 145);
--muted-foreground: oklch(0.7 0.02 145);
--accent: oklch(0.4 0.1 145);
--accent-foreground: oklch(0.85 0.05 145);
--destructive: oklch(0.6 0.2 25);
--destructive-foreground: oklch(1 0 0);
--border: oklch(0.35 0.03 145);
--input: oklch(0.35 0.03 145);
--ring: oklch(0.6 0.15 145);
}Step 2: Define Theme Configuration
import type { ThemeConfig } from "shadcn-theme-switcher";
import "./custom-themes.css";
export const myCustomThemes: ThemeConfig[] = [
{
name: "forest-green",
title: "Forest Green",
description: "Inspired by nature",
fonts: [
{ name: "Poppins", weights: [400, 600, 700] },
{ name: "Fira Code" }
]
}
];Step 3: Import and Use
import { ThemeSwitcher } from "shadcn-theme-switcher";
import { myCustomThemes } from "./custom-themes";
// Not required if already imported in your custom-themes file
import "./custom-themes.css";
function App() {
return <ThemeSwitcher themes={myCustomThemes} />;
}API Reference
Utility Functions
applyMode(mode: ThemeMode): void
Manually apply a theme mode to the document.
import { applyMode } from "shadcn-theme-switcher";
applyMode("dark"); // Adds 'dark' class to document.documentElementapplyTheme(themeName: string): void
Manually apply a theme to the document.
import { applyTheme } from "shadcn-theme-switcher";
applyTheme("violet-bloom"); // Sets data-theme="violet-bloom" on document.documentElementapplyThemeFonts(fonts: ThemeFont[]): void
Manually load Google Fonts.
import { applyThemeFonts } from "shadcn-theme-switcher";
applyThemeFonts([
{ name: "Inter", weights: [400, 600] },
{ name: "Fira Code" }
]); // Smartly handles font link tagsgetSystemTheme(): "light" | "dark"
Get the current system color scheme preference.
import { getSystemTheme } from "shadcn-theme-switcher";
const systemPreference = getSystemTheme();
console.log(systemPreference); // "light" or "dark"getEffectiveMode(mode: ThemeMode): "light" | "dark"
Resolve "system" mode to actual light/dark value.
import { getEffectiveMode } from "shadcn-theme-switcher";
const effective = getEffectiveMode("system");
console.log(effective); // "light" or "dark" based on system preferenceAdvanced Usage
Headless Implementation
Create completely custom UI using the hooks:
import { useTheme, useThemeMode } from "shadcn-theme-switcher";
import { Sun, Moon, Laptop } from "lucide-react";
import "your-custom-themes.css"
function CustomThemeSwitcher() {
const { theme, setTheme } = useTheme();
const { mode, setMode, effectiveMode } = useThemeMode();
const themes = [
{ id: "default", name: "Default" },
{ id: "violet-bloom", name: "Violet" }
];
return (
<div className="flex gap-4">
{/* Mode Buttons */}
<div className="flex gap-2">
<button
onClick={() => setMode("light")}
className={mode === "light" ? "active" : ""}
>
<Sun size={20} />
</button>
<button
onClick={() => setMode("dark")}
className={mode === "dark" ? "active" : ""}
>
<Moon size={20} />
</button>
<button
onClick={() => setMode("system")}
className={mode === "system" ? "active" : ""}
>
<Laptop size={20} />
</button>
</div>
{/* Theme Buttons */}
<div className="flex gap-2">
{themes.map(t => (
<button
key={t.id}
onClick={() => setTheme(t.id)}
className={theme === t.id ? "active" : ""}
>
{t.name}
</button>
))}
</div>
</div>
);
}SSR/SSG Support
For Next.js or other SSR frameworks, prevent hydration mismatches:
"use client"; // For Next.js App Router
import { useEffect, useState } from "react";
import { ModeSwitcher } from "shadcn-theme-switcher";
function Navigation() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return <div className="w-32 h-10" />; // Placeholder
}
return <ModeSwitcher />;
}Cross-Tab Synchronization
Theme changes automatically sync across tabs. You can also listen to changes:
import { useEffect } from "react";
import { useTheme, useThemeMode } from "shadcn-theme-switcher";
function SyncedComponent() {
const { theme } = useTheme();
const { mode } = useThemeMode();
useEffect(() => {
console.log("Theme changed:", theme);
// React to theme changes from other tabs
}, [theme]);
useEffect(() => {
console.log("Mode changed:", mode);
// React to mode changes from other tabs
}, [mode]);
return <div>Current: {theme} ({mode})</div>;
}Programmatic Theme Loading
Load themes dynamically based on user preferences or API data:
import { useState, useEffect } from "react";
import { ThemeSwitcher } from "shadcn-theme-switcher";
import type { ThemeConfig } from "shadcn-theme-switcher";
function DynamicThemeSwitcher() {
const [themes, setThemes] = useState<ThemeConfig[]>([]);
useEffect(() => {
// Fetch themes from API
fetch("/api/themes")
.then(res => res.json())
.then(data => setThemes(data));
}, []);
useEffect(() => {
// Load the theme css file dynamically
const link = document.createElement("link");
link.type = "text/css";
link.rel = "stylesheet";
// Your theme css file destination
link.href = `/api/themes/${theme}/styles.css`;
document.head.appendChild(link);
return () => { document.head.removeChild(link); }
}, [theme])
if (themes.length === 0) {
return <div>Loading themes...</div>;
}
return <ThemeSwitcher themes={themes} />;
}Troubleshooting
CSS Not Loading
Problem: Theme colors aren't being applied.
Solutions:
Ensure CSS is imported - In some rare cases you might have to import the themes.css file in your entry point:
import "shadcn-theme-switcher/themes.css";Verify data-theme attribute - Check that
data-themeis set on the root element:// Open DevTools and inspect <html> element // Should see: <html data-theme="violet-bloom">Check CSS specificity - Ensure theme styles aren't being overridden:
/* Your theme CSS should target the data-theme attribute */ [data-theme="my-theme"] { --primary: oklch(0.6231 0.188 259.8145); }
Theme Flash on Load (FOUC)
Problem: You see a brief flash of the wrong theme when the page loads.
Solutions:
Add inline script - Prevent flash by setting theme before React hydrates:
<!-- In your index.html --> <script> try { const theme = localStorage.getItem('app-theme') || 'default'; const mode = localStorage.getItem('app-theme-mode') || 'system'; document.documentElement.setAttribute('data-theme', theme); if (mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.documentElement.classList.add('dark'); } } catch (e) {} </script>For Next.js, use the
next-themespattern:// app/layout.tsx export default function RootLayout({ children }) { return ( <html suppressHydrationWarning> <head> <script dangerouslySetInnerHTML={{ __html: ` try { const theme = localStorage.getItem('app-theme') || 'default'; const mode = localStorage.getItem('app-theme-mode') || 'system'; document.documentElement.setAttribute('data-theme', theme); if (mode === 'dark' || (mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.documentElement.classList.add('dark'); } } catch (e) {} ` }} /> </head> <body>{children}</body> </html> ); }
Fonts Not Loading
Problem: Google Fonts specified in theme config aren't loading.
Solutions:
Check font names - Ensure font names match Google Fonts exactly:
// ✅ Correct fonts: [{ name: "Plus Jakarta Sans" }] // ❌ Incorrect fonts: [{ name: "Plus-Jakarta-Sans" }]Apply fonts manually if needed:
import { applyThemeFonts } from "shadcn-theme-switcher"; useEffect(() => { applyThemeFonts([ { name: "Inter", weights: [400, 600, 700] } ]); }, []);Use CSS font-family - Reference the loaded font in your CSS:
[data-theme="my-theme"] { font-family: "Inter", sans-serif; }
Theme Not Persisting
Problem: Theme resets on page reload.
Solutions:
Check localStorage access - Ensure localStorage is available:
// Test in browser console localStorage.setItem('test', 'value'); console.log(localStorage.getItem('test')); // Should log 'value'Verify storage keys - Check that the correct keys are being used:
// Theme key: 'app-theme' // Mode key: 'app-theme-mode' console.log(localStorage.getItem('app-theme')); console.log(localStorage.getItem('app-theme-mode'));Private/Incognito mode - localStorage may be disabled in private browsing.
Styles Conflict with shadcn/ui
Problem: Theme switcher styles conflict with your shadcn/ui components.
Solutions:
Namespace conflict - The package uses
.shadcn-theme-switcherclass:// All components are wrapped with this class <div className="shadcn-theme-switcher">...</div>Override styles - Use higher specificity if needed:
.my-app .shadcn-theme-switcher { /* Your overrides */ }Use custom implementation - Build your own UI with the hooks:
import { useTheme } from "shadcn-theme-switcher"; // Build custom component without using pre-built components
Cross-Tab Sync Not Working
Problem: Theme changes don't sync across browser tabs.
Solutions:
Check if using same origin - Cross-tab sync only works on same domain/port.
Test storage events - Verify events are firing:
useEffect(() => { const handler = (e: StorageEvent) => { console.log("Storage changed:", e.key, e.newValue); }; window.addEventListener("storage", handler); return () => window.removeEventListener("storage", handler); }, []);Same tab updates - Use custom events for updates in the same tab (already handled by the package).
Browser Support
- Chrome/Edge 90+
- Firefox 88+
- Safari 14+
- Modern mobile browsers
Requires support for:
- CSS custom properties
prefers-color-schememedia query- localStorage API
- CustomEvent API
Contributing
Contributions are welcome! Please check the GitHub repository for guidelines.
License
MIT © Nishant Mogha
Credits
Built with:
Need help? Open an issue or check existing discussions.
