React + MotionButtons

Loading to success button

Click it and the label turns into a spinner, then into a check mark, before the button resets itself.

Settings

24px
1

Code

import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Check, Loader2 } from "lucide-react";

interface LoadingButtonProps {
  color?: string;
  radius?: number;
  text?: string;
  /** Multiplica la velocidad de la carga simulada. */
  intensity?: number;
}

export default function LoadingButton({
  color = "#0071e3",
  radius = 24,
  text = "Save changes",
  intensity = 1,
}: LoadingButtonProps) {
  const [state, setState] = useState<"idle" | "loading" | "success">("idle");

  return (
    <motion.button
      type="button"
      className="flex min-w-36 items-center justify-center gap-2 px-6 py-3 text-sm font-semibold text-white shadow-lg"
      style={{
        backgroundColor: state === "success" ? "#0071e3" : color,
        borderRadius: radius,
      }}
      animate={{ scale: state === "loading" ? 0.97 : 1 }}
      onClick={() => {
        if (state !== "idle") return;
        setState("loading");
        setTimeout(() => setState("success"), 1500 / intensity);
        setTimeout(() => setState("idle"), 1500 / intensity + 1800);
      }}
    >
      <AnimatePresence mode="wait" initial={false}>
        {state === "idle" && (
          <motion.span
            key="idle"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          >
            {text}
          </motion.span>
        )}
        {state === "loading" && (
          <motion.span
            key="loading"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
          >
            <Loader2 className="h-4 w-4 animate-spin" />
          </motion.span>
        )}
        {state === "success" && (
          <motion.span
            key="success"
            initial={{ opacity: 0, scale: 0.5 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0 }}
            className="flex items-center gap-1.5"
          >
            <Check className="h-4 w-4" /> Done
          </motion.span>
        )}
      </AnimatePresence>
    </motion.button>
  );
}

Prompt for AI

Paste it into ChatGPT, Claude or Cursor and it will adapt the block to your project with these settings, even if you don’t use React.

More in Buttons

Where to use it

People click twice when nothing seems to happen. This button answers right away: it shows it is working, then confirms. Good for “Save changes” on a settings page, “Send message” on a contact form or “Apply coupon” at checkout. Idle, loading and done: those three states are what most forms need from a loading button.

In the demo the wait is simulated, so hook the success state to your real request. If saving can fail, add an error state too. Showing a check when nothing was saved does more harm than having no animation at all.