STUDY / 003 · Inputs
Spring toggle
A tiny interaction with just enough spring.
Preview
A little brighter.
Live preview · Go ahead, interact with it
Code
"use client";
import { Check, Moon, Sun } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import { useState } from "react";
import { cn } from "@/lib/utils";
export default function SpringToggle({ className }: { className?: string }) {
const [on, setOn] = useState(true);
const reduced = useReducedMotion();
return (
<div className={cn("flex flex-col items-center gap-6", className)}>
<div className="flex items-center gap-5">
<Moon
size={19}
strokeWidth={1.5}
className={
on ? "text-neutral-300" : "text-neutral-700 dark:text-neutral-300"
}
/>
<button
type="button"
role="switch"
aria-checked={on}
aria-label="Enable light mode"
onClick={() => setOn(!on)}
className={cn(
"flex h-12 w-21 items-center rounded-full p-1.5 transition-colors active:scale-95",
on ? "justify-end bg-[#c6e5a3]" : "justify-start bg-neutral-300",
)}
>
<motion.span
layout
transition={
reduced
? { duration: 0 }
: { type: "spring", stiffness: 500, damping: 28 }
}
className="grid size-9 place-items-center rounded-full bg-white dark:bg-[#0a0a0a] shadow-sm"
>
<Check
size={15}
className={on ? "text-[#698b46]" : "text-neutral-300"}
/>
</motion.span>
</button>
<Sun
size={20}
strokeWidth={1.5}
className={
on ? "text-neutral-700 dark:text-neutral-300" : "text-neutral-300"
}
/>
</div>
<span className="font-mono text-[10px] text-neutral-400 dark:text-neutral-500">
{on ? "A little brighter." : "A little quieter."}
</span>
</div>
);
}
React · TypeScript · Tailwind CSS
Copy the component and the cn helper into your project.
bun add motion lucide-react clsx tailwind-merge