Day/Night Switch
A toggle switch that smoothly transitions between day and night themes with animated sun, moon, stars, and clouds.
Preview
Installation
Install dependencies
# Install Shadcn Switch and Label components
npx shadcn@latest add switch label
# Install Framer Motion for animations
npm install framer-motion
Create the component file
mkdir -p components/shsfui/switch && touch components/shsfui/switch/day-night-switch.tsx
Add the component code
Open the newly created file and paste the following code:
"use client";
import * as React from "react";
import {
motion,
AnimatePresence,
type Variants,
type MotionProps,
} from "framer-motion";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { cn } from "@/utils/cn";
type DayNightSwitchProps = {
defaultChecked?: boolean;
onToggle?: (checked: boolean) => void;
} & React.HTMLAttributes<HTMLDivElement> &
MotionProps;
type AnimationMode = keyof typeof backgroundVariants;
const backgroundVariants: Variants = {
day: {
background: "linear-gradient(to bottom, #87CEEB, #E0F7FA)",
transition: { duration: 0.7 },
},
sunset: {
background: "linear-gradient(to bottom, #FF7E5F, #FEB47B, #D76D77)",
transition: { duration: 0.7 },
},
night: {
background: "linear-gradient(to bottom, #0F2027, #203A43, #2C5364)",
transition: { duration: 0.7 },
},
};
const sunVariants: Variants = {
visible: { y: 0, opacity: 1 },
sunset: { y: 24, opacity: 0.9, scale: 1.2, transition: { duration: 0.7 } },
hidden: { y: 40, opacity: 0, transition: { duration: 0.4 } },
};
const moonVariants: Variants = {
hidden: { y: -30, opacity: 0 },
rising: { y: 0, opacity: 1, transition: { delay: 0.5, duration: 0.7 } },
};
const cloudVariants: Variants = {
visible: { opacity: 0.9, x: 0 },
hidden: { opacity: 0, x: -30, transition: { duration: 0.5 } },
};
const createStarVariants = (index: number): Variants => ({
hidden: { opacity: 0, scale: 0 },
visible: {
opacity: [0, 0.8, 0.6 + Math.random() * 0.4],
scale: [0, 0.8 + Math.random() * 0.4, 0.6 + Math.random() * 0.4],
transition: {
delay: 0.7 + index * 0.12,
duration: 0.8,
},
},
});
const DayNightSwitch = React.forwardRef<HTMLDivElement, DayNightSwitchProps>(
({ className, defaultChecked = true, onToggle, ...restProps }, ref) => {
const id = React.useId();
const [checked, setChecked] = React.useState<boolean>(defaultChecked);
const handleToggle = (newValue: boolean) => {
setChecked(newValue);
onToggle?.(newValue);
};
const currentMode: AnimationMode = checked ? "day" : "night";
return (
<motion.div
ref={ref}
className={cn(
"relative w-20 h-10 rounded-md overflow-hidden border shadow",
className
)}
variants={backgroundVariants}
animate={currentMode}
initial={currentMode}
{...restProps}
>
<div className="relative h-full w-full">
<AnimatePresence>
{checked && (
<motion.div
className="absolute w-6 h-6 bg-yellow-400 rounded-full"
style={{
left: "25%",
top: "50%",
marginTop: -12,
marginLeft: -12,
}}
variants={sunVariants}
initial="visible"
animate={checked ? "visible" : "sunset"}
exit="hidden"
>
<SunRays />
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{!checked && (
<motion.div
className="absolute w-5 h-5"
style={{
left: "75%",
top: "50%",
marginTop: -10,
marginLeft: -10,
}}
variants={moonVariants}
initial="hidden"
animate={!checked ? "rising" : "hidden"}
>
<Moon />
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>{checked && <Clouds />}</AnimatePresence>
<AnimatePresence>{!checked && <Stars count={10} />}</AnimatePresence>
<div className="absolute inset-0 flex items-center justify-center">
<Switch
id={id}
checked={checked}
onCheckedChange={handleToggle}
className={cn(
"peer data-[state=unchecked]:bg-transparent data-[state=checked]:bg-transparent absolute inset-0 h-[inherit] w-auto [&_span]:z-10 [&_span]:size-6 [&_span]:border [&_span]:shadow [&_span]:rounded-sm [&_span]:transition-transform [&_span]:duration-500 [&_span]:[transition-timing-function:cubic-bezier(0.16,1,0.3,1)] [&_span]:data-[state=checked]:translate-x-10 [&_span]:data-[state=unchecked]:translate-x-2 [&_span]:bg-white [&_span]:border-gray-300"
)}
/>
</div>
</div>
<Label htmlFor={id} className="sr-only">
Day/Night Theme Switch
</Label>
</motion.div>
);
}
);
const SunRays = () => (
<>
{[...Array(8)].map((_, i) => (
<div
key={`ray-${i}`}
className="absolute bg-yellow-300 w-1 h-2"
style={{
left: "50%",
top: "50%",
transformOrigin: "0 0",
transform: `rotate(${
i * 45
}deg) translate(-50%, -50%) translate(10px, 0)`,
}}
/>
))}
</>
);
const Moon = () => (
<div className="relative w-full h-full">
<div className="absolute inset-0 bg-gray-100 rounded-full" />
<div
className="absolute bg-[#0F2027] rounded-full"
style={{
width: "90%",
height: "90%",
top: "-10%",
left: "-25%",
}}
/>
</div>
);
const Clouds = () => (
<>
<motion.div
className="absolute left-[60%] top-[30%] w-8 h-3 bg-white rounded-full opacity-90"
variants={cloudVariants}
initial="visible"
animate="visible"
exit="hidden"
/>
<motion.div
className="absolute left-[70%] top-[60%] w-6 h-2.5 bg-white rounded-full opacity-80"
variants={cloudVariants}
initial="visible"
animate="visible"
exit="hidden"
/>
</>
);
type StarsProps = {
count: number;
};
const Stars = ({ count }: StarsProps) => (
<>
{[...Array(count)].map((_, i) => (
<motion.div
key={`star-${i}`}
className="absolute w-0.5 h-0.5 bg-white rounded-full"
style={{
left: `${10 + i * 8}%`,
top: `${20 + (i % 5) * 12}%`,
boxShadow: "0 0 2px 1px rgba(255, 255, 255, 0.4)",
}}
variants={createStarVariants(i)}
initial="hidden"
animate="visible"
exit="hidden"
/>
))}
</>
);
DayNightSwitch.displayName = "DayNightSwitch";
export DayNightSwitch;