A pill button that cycles through loading, success, and error states with animated label swaps and a shake on error.
"use client";
import { cn } from "@/lib/utils";
import {
AnimatePresence,
motion as m,
Transition,
useAnimate,
Variants,
} from "motion/react";
import { useEffect, useState } from "react";
type Status = "Loading" | "Success" | "Error";
const sequences: Status[] = ["Loading", "Success", "Loading", "Error"];
const SEQUENCEDURATION = 2000;
const leftSpanVariant: Variants = {
hidden: { scale: 0.9, opacity: 0, x: -10 },
visible: { scale: 1, opacity: 1, x: 0 },
};
const rightSpanVariant: Variants = {
hidden: { scale: 0.9, opacity: 0, x: 10 },
visible: { scale: 1, opacity: 1, x: 0 },
};
const spanTransition: Transition = {
type: "spring",
bounce: 0,
duration: 0.3,
};
const StatusButton = () => {
const [status, setStatus] = useState<Status>("Loading");
const [scope, animate] = useAnimate();
useEffect(() => {
if (status === "Error") {
setTimeout(() => {
animate(
scope.current,
{ rotate: [0, 20, -20, 0, 20, -20, 0] },
{ duration: 0.2 }
);
}, 750);
}
}, [status]);
useEffect(() => {
let currentIndex = 0;
const interval = setInterval(() => {
setStatus(sequences[currentIndex]);
currentIndex = (currentIndex + 1) % sequences.length;
}, SEQUENCEDURATION);
return () => clearInterval(interval);
}, []);
return (
<div className="full center">
<m.button
className={cn(
status === "Loading" &&
"bg-blue-100 shadow-none hover:bg-blue-100 text-blue-500",
status === "Success" &&
"bg-green-100 shadow-none hover:bg-green-100 text-green-500",
status === "Error" &&
"bg-red-100 shadow-none text-destructive hover:bg-red-100",
"rounded-full overflow-hidden flex items-center gap-2 px-4 py-2"
)}
layout
layoutId="status-button"
transition={{
type: "spring",
stiffness: 300,
damping: 30,
mass: 0.5,
}}
>
<m.span
className="whitespace-pre flex items-center gap-1"
transition={spanTransition}
>
<AnimatePresence initial={false} mode="wait">
{status === "Loading" && (
<m.span
variants={leftSpanVariant}
initial="hidden"
animate="visible"
>
Analyzing
</m.span>
)}
{status === "Success" && (
<m.span
variants={rightSpanVariant}
initial="hidden"
animate="visible"
>
Safe
</m.span>
)}
{status === "Error" && (
<m.span
variants={rightSpanVariant}
initial="hidden"
animate="visible"
>
Warning
</m.span>
)}
</AnimatePresence>
</m.span>
</m.button>
</div>
);
};
export default StatusButton;