Envoi en avion en papier
L’avion prend son élan, décolle du bouton qui rétrécit, revient sous forme de coche dessinée puis regagne sa place en planant.
Réglages
Code
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { Send } from "lucide-react";
type SendState = "idle" | "flying" | "sent";
interface PaperPlaneSendProps {
label?: string;
sentLabel?: string;
color?: string;
}
const sendSpring = { type: "spring" as const, stiffness: 520, damping: 34 };
const sendText = {
initial: { opacity: 0, y: 10, filter: "blur(4px)" },
animate: { opacity: 1, y: 0, filter: "blur(0px)" },
exit: { opacity: 0, y: -10, filter: "blur(4px)" },
};
export default function PaperPlaneSend({
label = "Envoyer",
sentLabel = "Envoyé",
color = "#0a84ff",
}: PaperPlaneSendProps) {
const [state, setState] = useState<SendState>("idle");
const [trip, setTrip] = useState(0);
const timers = useRef<ReturnType<typeof setTimeout>[]>([]);
const reduce = useReducedMotion();
useEffect(() => () => timers.current.forEach(clearTimeout), []);
const send = () => {
if (state !== "idle") return;
setState("flying");
timers.current = [
setTimeout(() => setState("sent"), reduce ? 0 : 560),
setTimeout(() => {
setState("idle");
setTrip((t) => t + 1);
}, 2400),
];
};
return (
<motion.button
type="button"
layout
onClick={send}
aria-disabled={state !== "idle"}
whileTap={{ scale: state === "idle" ? 0.95 : 1 }}
transition={sendSpring}
className="relative flex h-12 items-center justify-center gap-2 px-6 text-[15px] font-medium text-white outline-none focus-visible:ring-2 focus-visible:ring-black/25 focus-visible:ring-offset-2 dark:focus-visible:ring-white/40 dark:focus-visible:ring-offset-black"
style={{
backgroundColor: color,
borderRadius: 999,
boxShadow: "inset 0 1px 0 rgba(255,255,255,0.28), inset 0 -1px 0 rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.14), 0 10px 24px -10px " + color,
}}
>
<motion.span layout="position" className="relative grid h-[18px] w-[18px] place-items-center">
<AnimatePresence initial={false}>
{state === "sent" ? (
<motion.svg
key="check"
viewBox="0 0 24 24"
className="absolute h-[18px] w-[18px]"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
exit={{ opacity: 0, scale: 0.6 }}
transition={{ duration: 0.15 }}
>
<motion.path
d="M5 12.5l4.5 4.5L19 7.5"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.32, ease: [0.65, 0, 0.35, 1] }}
/>
</motion.svg>
) : (
<motion.span
key={"plane-" + trip}
className="absolute grid place-items-center"
initial={trip === 0 ? false : { x: -26, y: 12, rotate: 18, opacity: 0 }}
animate={
state === "flying" && !reduce
? {
x: [0, -4, 110],
y: [0, 3, -80],
rotate: [0, 14, -10],
scale: [1, 0.92, 0.7],
opacity: [1, 1, 0],
filter: ["blur(0px)", "blur(0px)", "blur(1.5px)"],
}
: { x: 0, y: 0, rotate: 0, scale: 1, opacity: 1 }
}
exit={{ opacity: 0 }}
transition={
state === "flying"
? { duration: 0.56, times: [0, 0.28, 1], ease: [0.55, 0, 0.8, 0.2] }
: { type: "spring", stiffness: 260, damping: 22 }
}
>
<Send className="h-[18px] w-[18px]" strokeWidth={1.75} />
</motion.span>
)}
</AnimatePresence>
</motion.span>
<AnimatePresence mode="popLayout" initial={false}>
{state !== "flying" && (
<motion.span
key={state}
layout="position"
className="whitespace-nowrap leading-5"
initial={sendText.initial}
animate={sendText.animate}
exit={sendText.exit}
transition={sendSpring}
>
{state === "sent" ? sentLabel : label}
</motion.span>
)}
</AnimatePresence>
<span className="sr-only" aria-live="polite">
{state === "sent" ? sentLabel : ""}
</span>
</motion.button>
);
}
Prompt pour l’IA
Collez-le dans ChatGPT, Claude ou Cursor : le bloc sera adapté à votre projet avec ces réglages, même sans React.
Plus dans Micro-interactions
Où l’utiliser
Une animation sur le bouton d’envoi confirme que le message est bien parti, ce qui compte plus qu’on ne le croit sur un formulaire de contact, une inscription à la newsletter ou une messagerie. Ici l’avion en papier prend son élan, s’envole, et une coche se dessine à côté de « Envoyé ». Puis il revient en planant.
Déclenchez le vol une fois le formulaire vraiment envoyé, pas au clic, sinon vous promettez quelque chose qui peut échouer. L’aller-retour dure un peu plus de deux secondes. Gardez deux libellés courts pour que le bouton ne change pas trop de largeur.