Back
An Experiment In Toggle Switch Animation
July 2023
CSS
Javascript
Framer Motion
Here's a fun animation that I created for a Toggle Switch component with some basic styling. I took inspiration from a great twitter post by Derek Briggs.
The animation is created with Framer Motion. Code snippet to follow.
Give it a try!
Code:
import React, { useState } from "react";
import { cubicBezier, useAnimate } from "framer-motion";
import { Check, IconWeight, Minus } from "@phosphor-icons/react";
import { FontWeight } from "next/dist/compiled/@vercel/og/satori";
function ToggleSwitch() {
const [isChecked, setIsChecked] = useState(false);
const [thumbScope, thumbAnimate] = useAnimate();
const [iconScope, iconAnimate] = useAnimate();
const customEaseIn = cubicBezier(0.47, 0.15, 0.86, 0.55);
const customEaseOut = cubicBezier(0.13, 0.47, 0.52, 0.9);
const handleIconLeave = async () => {
await iconAnimate(
iconScope.current,
{
opacity: 0,
scale: 0.5,
rotate: 30,
},
{
duration: 0.12,
ease: customEaseIn,
}
);
};
const handleIconEnter = async () => {
await iconAnimate(
iconScope.current,
{
opacity: 1,
scale: 1,
rotate: 0,
},
{
duration: 0.12,
ease: customEaseOut,
}
);
};
const handleChange = async (checked: boolean) => {
await handleIconLeave();
await thumbAnimate(
thumbScope.current,
{
width: "100%",
backgroundColor: "#AEB7C3",
},
{ duration: 0.18, ease: customEaseIn }
);
setIsChecked(checked);
await thumbAnimate(
thumbScope.current,
{
width: "auto",
backgroundColor: checked ? "#64748b" : "#f8fafc",
},
{
duration: 0.18,
ease: customEaseOut,
}
);
handleIconEnter();
};
const commonIconProps: { weight: IconWeight; size: number } = {
weight: "bold",
size: 16,
};
return (
<div
className={`bg-slate-200 border-slate-300 rounded-full w-14 h-8 p-1 flex relative cursor-pointer ${
isChecked ? "justify-end" : "justify-start"
}`}
onClick={() => handleChange(!isChecked)}
>
<div
ref={thumbScope}
className={`${
isChecked ? "bg-slate-500" : "bg-slate-50"
} rounded-full z-10 transition-transform`}
>
<div
ref={iconScope}
className="h-6 w-6 flex items-center justify-center"
>
{isChecked ? (
<Check
className="text-slate-100 cursor-pointer"
{...commonIconProps}
/>
) : (
<Minus
className="text-slate-400 cursor-pointer"
{...commonIconProps}
></Minus>
)}
</div>
</div>
<input
id="checkbox-test"
className="opacity-0 absolute top-0 left-0 w-full h-full cursor-pointer"
type="checkbox"
aria-label="toggle switch"
data-checked={isChecked}
checked={isChecked}
/>
</div>
);
}
export default ToggleSwitch;