Skip to content

Instantly share code, notes, and snippets.

@koohz
Created April 28, 2025 12:51
Show Gist options
  • Save koohz/93ea51f85769456ed13817b58e66bf57 to your computer and use it in GitHub Desktop.
Save koohz/93ea51f85769456ed13817b58e66bf57 to your computer and use it in GitHub Desktop.
Shadcn-ui theme switcher (motion + cva)
"use client";
import { cn } from "@/lib/utils";
import { Monitor, Moon, Sun } from "lucide-react";
import { motion } from "framer-motion";
import { useEffect, useId, useState } from "react";
import { useTheme } from "next-themes";
import { cva, VariantProps } from "class-variance-authority";
const themeSwitcherVariants = cva(
"bg-background ring-border relative isolate flex rounded-full ring-1 w-min p-1",
{
variants: {
size: {
default: "h-8",
sm: "h-7",
lg: "h-9",
},
},
defaultVariants: {
size: "default",
},
},
);
const themes = [
{
key: "system",
icon: Monitor,
label: "System theme",
},
{
key: "light",
icon: Sun,
label: "Light theme",
},
{
key: "dark",
icon: Moon,
label: "Dark theme",
},
];
export type ThemeSwitcherProps = {
className?: string;
} & VariantProps<typeof themeSwitcherVariants>;
export const ThemeSwitcher = ({ className, size }: ThemeSwitcherProps) => {
const [mounted, setMounted] = useState(false);
const { theme, setTheme } = useTheme();
const id = useId();
// Prevent hydration mismatch
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null;
}
return (
<div className={cn(themeSwitcherVariants({ size }), className)}>
{themes.map(({ key, icon: Icon, label }) => {
const isActive = theme === key;
return (
<button
type="button"
key={key}
className={cn(
"relative rounded-full",
size === "sm" && "size-5",
size === "default" && "size-6",
size === "lg" && "size-7",
)}
onClick={() => setTheme(key as "light" | "dark" | "system")}
aria-label={label}
>
{isActive && (
<motion.div
layoutId={id}
className="bg-secondary absolute inset-0 rounded-full"
transition={{ type: "spring", duration: 0.5 }}
/>
)}
<Icon
className={cn(
"relative z-10 m-auto",
isActive ? "text-foreground" : "text-muted-foreground",
size === "sm" && "size-3",
size === "default" && "size-3.5",
size === "lg" && "size-4",
)}
/>
</button>
);
})}
</div>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment