Installation
CLI
npx shadcn@latest add https://kelvinmai.io/r/theme-switch.json
Manual
Install the following dependencies
npm install next-themes lucide-react motion clsx tailwind-merge
Add a cn helper
lib/utils.ts
import { clsx, type ClassValue } from 'clsx';import { twMerge } from 'tailwind-merge';export const cn = (...inputs: ClassValue[]) => {return twMerge(clsx(inputs));};
Set up next-themes in your Next.js app
app/layout.tsx
import { ThemeProvider } from 'next-themes';export default function RootLayout({children,}: Readonly<{children: React.ReactNode;}>) {return (<ThemeProvider attribute='class' defaultTheme='system' enableSystem>{children}</ThemeProvider>);}
Copy and paste the following code into your project
components/theme-switch.tsx
'use client';import React, { type JSX, useEffect, useState } from 'react';import { MonitorIcon, MoonStarIcon, SunIcon } from 'lucide-react';import { motion } from 'motion/react';import { useTheme } from 'next-themes';import { cn } from '@/lib/utils';const ThemeOption = ({icon,value,isActive,onClick,}: {icon: JSX.Element;value: string;isActive?: boolean;onClick: (value: string) => void;}) => {return (<buttonclassName={cn('relative flex size-8 cursor-default items-center justify-center rounded-full transition-all [&_svg]:size-4',isActive? 'text-zinc-950 dark:text-zinc-50': 'text-zinc-400 hover:text-zinc-950 dark:text-zinc-500 dark:hover:text-zinc-50',)}role='radio'aria-checked={isActive}aria-label={`Switch to ${value} theme`}onClick={() => onClick(value)}>{icon}{isActive && (<motion.divlayoutId='theme-option'transition={{ type: 'spring', bounce: 0.3, duration: 0.6 }}className='absolute inset-0 rounded-full border border-zinc-200 dark:border-zinc-700'/>)}</button>);};const THEME_OPTIONS = [{icon: <MonitorIcon />,value: 'system',},{icon: <SunIcon />,value: 'light',},{icon: <MoonStarIcon />,value: 'dark',},];const ThemeSwitch = () => {const { theme, setTheme } = useTheme();const [isMounted, setIsMounted] = useState(false);useEffect(() => {setIsMounted(true);}, []);if (!isMounted) {return <div className='flex h-8 w-24' />;}return (<motion.divkey={String(isMounted)}initial={{ opacity: 0 }}animate={{ opacity: 1 }}transition={{ duration: 0.3 }}className='inline-flex items-center overflow-hidden rounded-full bg-white ring-1 ring-zinc-200 ring-inset dark:bg-zinc-950 dark:ring-zinc-700'role='radiogroup'>{THEME_OPTIONS.map((option) => (<ThemeOptionkey={option.value}icon={option.icon}value={option.value}isActive={theme === option.value}onClick={setTheme}/>))}</motion.div>);};export { ThemeSwitch };
Update the import paths to match your project setup
Usage
import { ThemeSwitcher } from '@/components/theme-switcher';<ThemeSwitcher />;