Bouton copier la commande
À la copie, un surlignage balaie la commande et l’icône se change en coche qui se dessine toute seule.
Réglages
Code
import { useEffect, useRef, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
import { Copy } from "lucide-react";
interface CopyCommandProps {
command?: string;
accent?: string;
}
export default function CopyCommand({
command = "npm i @aimran/ui",
accent = "#22c55e",
}: CopyCommandProps) {
const [copied, setCopied] = useState(false);
const [sweep, setSweep] = useState(0);
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
const reduce = useReducedMotion();
useEffect(() => () => clearTimeout(timer.current), []);
const copy = () => {
try {
navigator.clipboard?.writeText(command).catch(() => {});
} catch {
// Sin portapapeles disponible: la animación sigue funcionando
}
setCopied(true);
setSweep((s) => s + 1);
clearTimeout(timer.current);
timer.current = setTimeout(() => setCopied(false), 1800);
};
return (
<div className="flex w-full max-w-[19rem] items-center gap-3 rounded-2xl bg-white py-1.5 pl-4 pr-1.5 font-mono text-[13px] text-[#1d1d1f] shadow-[0_0_0_1px_rgba(0,0,0,0.06),0_1px_2px_rgba(0,0,0,0.04),0_8px_24px_rgba(0,0,0,0.06)] dark:bg-[#1c1c1e] dark:text-[#f5f5f7] dark:shadow-[0_0_0_1px_rgba(255,255,255,0.08),0_8px_24px_rgba(0,0,0,0.4)]">
<span className="select-none text-[#86868b]" aria-hidden="true">
$
</span>
<span className="relative min-w-0 flex-1">
{sweep > 0 && !reduce && (
<motion.span
key={sweep}
aria-hidden="true"
className="absolute -inset-x-1 -inset-y-0.5 origin-left rounded-[5px]"
style={{ backgroundColor: accent }}
initial={{ scaleX: 0, opacity: 0.24 }}
animate={{ scaleX: 1, opacity: [0.24, 0.24, 0] }}
transition={{
scaleX: { duration: 0.34, ease: [0.22, 1, 0.36, 1] },
opacity: { duration: 1.1, times: [0, 0.55, 1] },
}}
/>
)}
<span className="relative block truncate">{command}</span>
</span>
<motion.button
type="button"
onClick={copy}
aria-label={copied ? "Copié" : "Copier la commande"}
whileTap={{ scale: 0.88 }}
transition={{ type: "spring", stiffness: 600, damping: 30 }}
className="relative grid h-9 w-9 shrink-0 place-items-center rounded-xl text-[#86868b] outline-none transition-colors hover:bg-black/5 hover:text-[#1d1d1f] focus-visible:ring-2 focus-visible:ring-black/20 dark:hover:bg-white/10 dark:hover:text-[#f5f5f7] dark:focus-visible:ring-white/30"
>
<motion.span
aria-hidden="true"
className="absolute inset-0 rounded-xl"
style={{ backgroundColor: accent }}
initial={false}
animate={{ opacity: copied ? 0.14 : 0 }}
transition={{ duration: 0.2 }}
/>
<AnimatePresence mode="popLayout" initial={false}>
{copied ? (
<motion.svg
key="check"
viewBox="0 0 24 24"
className="relative h-[18px] w-[18px]"
fill="none"
stroke={accent}
strokeWidth={2.25}
strokeLinecap="round"
strokeLinejoin="round"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.5, filter: "blur(2px)" }}
transition={{ type: "spring", stiffness: 500, damping: 26 }}
>
<motion.path
d="M5 12.5l4.5 4.5L19 7.5"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.3, delay: 0.05, ease: [0.65, 0, 0.35, 1] }}
/>
</motion.svg>
) : (
<motion.span
key="copy"
className="relative grid place-items-center"
initial={{ opacity: 0, scale: 0.5, filter: "blur(2px)" }}
animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
exit={{ opacity: 0, scale: 0.5, filter: "blur(2px)" }}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
>
<Copy className="h-[18px] w-[18px]" strokeWidth={1.5} />
</motion.span>
)}
</AnimatePresence>
</motion.button>
<span className="sr-only" aria-live="polite">
{copied ? "Copié dans le presse-papiers" : ""}
</span>
</div>
);
}
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
Un bouton copier dans le presse-papiers fait partie des détails qu’on remarque surtout quand ils manquent. Il a sa place à côté de la commande d’installation d’une documentation, d’une clé d’API dans un tableau de bord, d’un code promo ou d’un lien de parrainage. Le balayage montre ce qui a été copié, et les lecteurs d’écran annoncent « Copié ».
Les navigateurs n’autorisent la copie que sur des pages en HTTPS ; en simple HTTP, l’animation se joue mais rien n’est copié. Les commandes longues sont tronquées ; si la vôtre l’est, élargissez la boîte. La coche verte se lit comme un succès presque partout.