Skip to main content

ThemeSwitcher

A dropdown menu component that allows users to switch between color themes and toggle between light/dark modes. Built with shadcn/ui components and fully integrated with next-themes.
Located in registry/nextjs/components/theme-switcher.tsx

Component Signature

export function ThemeSwitcher(): JSX.Element

Props

This component accepts no props. All configuration is handled internally through the useTheme hook.

Features

  • Dual Theme Controls: Separate controls for color theme and light/dark mode
  • Visual Theme Previews: Color dots showing each theme’s primary color
  • Current Theme Indicator: Check marks on active selections
  • Scrollable Theme List: Smooth scrolling for 45+ themes
  • SSR Safe: Prevents hydration mismatches with mounted state
  • Keyboard Accessible: Full keyboard navigation support

Usage

import { ThemeSwitcher } from "@/components/theme-switcher";

export default function Header() {
  return (
    <header>
      <nav>
        <h1>My App</h1>
        <ThemeSwitcher />
      </nav>
    </header>
  );
}

UI Structure

Trigger Button

Button
Component
Ghost variant icon button (9×9) displaying Sun (light mode) or Moon (dark mode) icon.
<Button variant="ghost" size="icon" className="h-9 w-9">
  {isDark ? <Moon className="h-4 w-4" /> : <Sun className="h-4 w-4" />}
</Button>
The dropdown menu contains three main sections:

1. Current Theme Label

Label
DropdownMenuLabel
Shows the current theme name with a colored preview dot.
<DropdownMenuLabel className="flex items-center gap-2">
  <div
    className="h-3 w-3 rounded-full"
    style={{ backgroundColor: isDark ? primaryDark : primaryLight }}
  />
  {currentThemeConfig.title}
</DropdownMenuLabel>

2. Mode Toggle (Light/Dark)

Mode Items
DropdownMenuItem[]
Two items for switching between light and dark modes. Active mode shows a check mark.
<DropdownMenuItem onClick={() => setMode("light")}>
  <Sun className="h-4 w-4" />
  <span>Light</span>
  {mode === "light" && <Check className="h-4 w-4" />}
</DropdownMenuItem>

3. Color Theme Submenu

Theme Submenu
DropdownMenuSub
Scrollable submenu with all 45 themes, each showing a color preview.
<DropdownMenuSub>
  <DropdownMenuSubTrigger>
    <Palette className="h-4 w-4" />
    <span>Color Theme</span>
  </DropdownMenuSubTrigger>
  <DropdownMenuSubContent>
    <ScrollArea className="h-80">
      {sortedThemes.map((theme) => (
        <DropdownMenuItem onClick={() => setColorTheme(theme.name)}>
          <div className="h-4 w-4 rounded-full" style={{ backgroundColor }} />
          <span>{theme.title}</span>
          {isActive && <Check className="h-4 w-4" />}
        </DropdownMenuItem>
      ))}
    </ScrollArea>
  </DropdownMenuSubContent>
</DropdownMenuSub>

Internal Functions

parseTheme

Helper function to extract color theme and mode from theme string.
function parseTheme(theme: string | undefined): {
  colorTheme: string;
  mode: "light" | "dark";
}
theme
string | undefined
Theme string like "catppuccin-dark" or "vercel-light"
Returns:
colorTheme
string
Theme name without mode suffix (e.g., "catppuccin")
mode
'light' | 'dark'
Extracted mode from theme string, defaults to "dark"
Examples:
parseTheme("catppuccin-dark")  // { colorTheme: "catppuccin", mode: "dark" }
parseTheme("vercel-light")     // { colorTheme: "vercel", mode: "light" }
parseTheme(undefined)          // { colorTheme: "default", mode: "dark" }

setColorTheme

Sets the color theme while preserving the current mode.
const setColorTheme = (name: string) => {
  setTheme(`${name}-${mode}`);
};
Example:
// Current: "default-dark"
setColorTheme("catppuccin");
// Result: "catppuccin-dark"

setMode

Sets the light/dark mode while preserving the current color theme.
const setMode = (newMode: "light" | "dark") => {
  setTheme(`${colorTheme}-${newMode}`);
};
Example:
// Current: "catppuccin-dark"
setMode("light");
// Result: "catppuccin-light"

Hydration Safety

The component uses a mounted state to prevent hydration mismatches:
const [mounted, setMounted] = useState(false);

useEffect(() => {
  setMounted(true);
}, []);

if (!mounted) {
  return (
    <Button variant="ghost" size="icon" className="h-9 w-9">
      <Sun className="h-4 w-4" />
    </Button>
  );
}
This ensures the server-rendered HTML matches the initial client render before showing the actual theme state.

Dependencies

Required Components (shadcn/ui)

  • Button - Trigger button component
  • DropdownMenu - Main menu container
  • DropdownMenuContent - Menu content wrapper
  • DropdownMenuItem - Individual menu items
  • DropdownMenuLabel - Menu section labels
  • DropdownMenuSeparator - Visual dividers
  • DropdownMenuSub - Nested submenu
  • DropdownMenuSubTrigger - Submenu trigger
  • DropdownMenuSubContent - Submenu content
  • DropdownMenuPortal - Portal for submenu rendering
  • ScrollArea - Scrollable theme list

Required Icons (lucide-react)

  • Sun - Light mode icon
  • Moon - Dark mode icon
  • Palette - Theme picker icon
  • Check - Active selection indicator

Required Utilities

  • useTheme from next-themes - Theme state management
  • cn from @/lib/utils - Class name utility
  • sortedThemes from @/lib/themes-config - Sorted theme list
  • themes from @/lib/themes-config - Theme configurations

Styling

The component uses Tailwind CSS classes and is fully customizable:
// Trigger button
<Button variant="ghost" size="icon" className="h-9 w-9">

// Dropdown content
<DropdownMenuContent align="end" className="w-48">

// Theme color preview
<div className="h-4 w-4 rounded-full border border-border" />

// Scrollable area
<ScrollArea className="h-80">

Accessibility

  • Screen Reader Support: Trigger button includes <span class="sr-only">Toggle theme</span>
  • Keyboard Navigation: Full keyboard support via DropdownMenu
  • Visual Indicators: Check marks for active selections
  • Color Previews: Visual dots help users identify themes

Advanced Customization

Custom Button Styling

Wrap and style the component:
<div className="rounded-lg border p-1">
  <ThemeSwitcher />
</div>

Fork for Custom Behavior

Copy and modify the component to:
  • Filter visible themes
  • Add custom theme groupings
  • Change icon sets
  • Modify menu structure
theme-switcher-custom.tsx
import { ThemeSwitcher } from "@/components/theme-switcher";

// Filter to specific themes
const filteredThemes = sortedThemes.filter(t => 
  ["default", "catppuccin", "vercel"].includes(t.name)
);

See Also

Build docs developers (and LLMs) love