Retour au catalogue

CTA Magnetic Burst

CTA section premium avec bouton magnetique (spring physics), burst de particules au hover, double ring pulsant et spotlight qui suit la souris. Style agence haut de gamme.

ctacomplex Both Responsive a11y
boldelegantagencysaasportfoliocentered
Theme

Créer un bouton CTA magnétique avec burst de particules en React

Un bouton magnétique en React se décale vers le curseur grâce à useMotionValue et useSpring : quand le pointeur entre dans un rayon de ~120px, les dx/dy par rapport au centre du bouton sont multipliés par 0.28 pour créer une légère attraction. Au hover, 8 particules éclatent en cercle via AnimatePresence, chacune animée selon un angle pré-calculé avec un délai échelonné.

  • Stack : React + Framer Motion 11 + lucide-react, ~113 lignes, zéro autre dépendance.
  • API clés : useMotionValue, useSpring, AnimatePresence, useCallback pour des handlers stables.
  • Accessible : les particules sont aria-hidden, les anneaux pulsants aussi, le texte du bouton est lisible.
  • Le spotlight de tracking souris utilise un dégradé radial statique centré dans la section, sans mise à jour inline piloté par JS, le parallax est décoratif, non précis au pixel.
  • L'attraction magnétique se désactive automatiquement quand le curseur sort (le spring revient à 0,0).

CTA Magnetic Burst est une section call-to-action centrée construite autour d'un bouton à haute intention qui attire physiquement le curseur, puis explose en particules au hover. Deux anneaux pulsants persistants maintiennent le bouton vivant quand la souris est ailleurs. Un spotlight radial et une lueur floue profonde donnent de l'atmosphère à la section sans recourir à des images.

Anatomie

La section comporte quatre couches superposées en position:absolute. En fond, une lueur statique profonde : un grand cercle flouté à 5% d'opacité. Au-dessus, une motion.div porte le spotlight en dégradé radial (centre statique, purement décoratif). La couche de contenu contient le h2, le paragraphe de description et le composant MagneticButton. Le bouton lui-même possède deux spans d'anneaux pulsants (aria-hidden), un ensemble de 8 spans BurstParticle contrôlés par AnimatePresence, et le label + icône ArrowRight en z-index 1 relatif.

Comment ça marche

L'attraction magnétique est mesurée à chaque mousemove : getBoundingClientRect donne le centre du bouton, puis dx = clientX - centerX et dy = clientY - centerY. Si l'hypoténuse est inférieure à 120px, x.set(dx * 0.28) et y.set(dy * 0.28), le facteur 0.28 garde l'attraction subtile. Ces valeurs brutes alimentent deux instances useSpring (stiffness 180, damping 14, mass 0.08) pour la translation, le bouton glisse donc au lieu de claquer. Le burst se déclenche dans handleHoverStart : l'état est remis à false, puis immédiatement repassé à true dans un requestAnimationFrame pour qu'AnimatePresence démonte et remonte les 8 particules à chaque hover, même en ré-entrées rapides. Chaque BurstParticle anime opacity [0,1,0], scale [0,1.4,0] et se translate vers un (tx,ty) pré-calculé via la formule du cercle unité en 0.55s.

Comment le coder en React

  1. Pré-calculer les positions des particules

    Génère les 8 cibles de particules une seule fois au niveau du module, pas dans le composant, pour éviter tout recalcul à chaque rendu. Divise un cercle complet (2π) en 8 angles égaux et projette chacun sur un rayon de 60px.

    const BURST_COUNT = 8;
    const BURST_RADIUS = 60;
    const burstParticles = Array.from({ length: BURST_COUNT }, (_, i) => {
      const angle = (i / BURST_COUNT) * Math.PI * 2;
      return { id: i, tx: Math.cos(angle) * BURST_RADIUS, ty: Math.sin(angle) * BURST_RADIUS };
    });
  2. Câbler le spring magnétique

    Crée deux motion values pour x et y, puis passe-les dans useSpring avec une masse faible pour que le décalage soit réactif. Dans le handler mousemove, n'applique l'attraction que quand le curseur est dans les 120px du centre du bouton, au-delà, le spring reste au repos.

    const x = useMotionValue(0);
    const y = useMotionValue(0);
    const springX = useSpring(x, { stiffness: 180, damping: 14, mass: 0.08 });
    const springY = useSpring(y, { stiffness: 180, damping: 14, mass: 0.08 });
    
    const handleMouseMove = (e: React.MouseEvent) => {
      const rect = btnRef.current!.getBoundingClientRect();
      const dx = e.clientX - (rect.left + rect.width / 2);
      const dy = e.clientY - (rect.top + rect.height / 2);
      if (Math.hypot(dx, dy) < 120) { x.set(dx * 0.28); y.set(dy * 0.28); }
    };
  3. Déclencher le burst via AnimatePresence

    L'astuce pour redéclencher les particules à chaque hover rapide est de remettre l'état à false puis à true dans un requestAnimationFrame, React démonte ainsi les particules précédentes avant d'en monter un nouvel ensemble. Enveloppe les particules mappées dans AnimatePresence pour que les animations de sortie s'exécutent.

    const handleHoverStart = useCallback(() => {
      setHovered(false);
      requestAnimationFrame(() => setHovered(true));
    }, []);
    
    // In JSX:
    <AnimatePresence>
      {hovered && burstParticles.map((p) =>
        <BurstParticle key={`${p.id}-${String(hovered)}`} tx={p.tx} ty={p.ty} delay={p.id * 0.04} />
      )}
    </AnimatePresence>
  4. Ajouter les anneaux pulsants à l'état de repos

    Deux spans motion partagent le même style d'anneau (position:absolute, inset:-3, bordure accent, border-radius full). Chacun boucle indéfiniment en passant de scale 1 à 1.22 tout en s'effaçant, décalé de 0.8s l'un par rapport à l'autre, le déphasage crée un effet de respiration continue sans avoir besoin d'aucun état.

    <motion.span aria-hidden style={ringStyle}
      animate={{ scale: [1, 1.22], opacity: [0.6, 0] }}
      transition={{ duration: 1.6, repeat: Infinity, ease: "easeOut" }} />
    <motion.span aria-hidden style={ringStyle}
      animate={{ scale: [1, 1.22], opacity: [0.4, 0] }}
      transition={{ duration: 1.6, repeat: Infinity, ease: "easeOut", delay: 0.8 }} />

Quand l'utiliser

Utilise cette section comme dernier appel à l'action avant le footer sur les sites d'agences, les pages marketing SaaS ou les portfolios où une seule conversion compte plus que plusieurs. L'attraction magnétique et le burst de particules sont conçus pour créer une impression finale mémorable, pas pour rivaliser avec les CTA produits inline plus haut sur la page. À éviter sur les tunnels de checkout e-commerce, les dashboards ou toute page où le bouton doit être immédiatement évident sans nécessiter de découverte au hover. L'effet magnétique requiert un pointeur, sur mobile le bouton fonctionne mais sans la physique.

Utilisé par

  • Stripe, Utilise des micro-interactions à attraction magnétique sur les boutons CTA clés de ses pages marketing pour augmenter la perception d'interactivité.
  • Linear, Emploie des effets de particules et de lueur sur les boutons d'action principaux de tout son site marketing produit.
  • Resend, Combine des anneaux pulsants et des lueurs accentuées sur les boutons CTA pour attirer l'attention sans animation lourde.

FAQ

L'effet magnétique fonctionne-t-il sur mobile ?

Le bouton reste entièrement fonctionnel sur écran tactile, mais il n'y a pas de pointeur pour déclencher l'attraction magnétique ni le burst de particules. Les anneaux pulsants continuent d'animer, ce qui maintient le bouton visuellement actif.

Pourquoi utiliser requestAnimationFrame pour re-déclencher le burst ?

React regroupe les mises à jour d'état dans le même frame, donc passer hovered à false puis à true de façon synchrone ne produit aucun re-render. L'appel rAF flush d'abord la mise à jour false, ce qui pousse AnimatePresence à démonter les particules précédentes ; le frame suivant monte un nouvel ensemble avec un état initial propre.

Peut-on réduire la masse du spring pour rendre l'attraction plus vive ?

Oui. La config actuelle est stiffness:180, damping:14, mass:0.08. Réduire la masse à 0.04 fait suivre le curseur presque instantanément ; la passer au-dessus de 0.2 produit un décalage plus lourd et dramatique. Ajuste damping en même temps que mass, trop peu de damping avec une masse très faible crée des oscillations.

Comment respecter prefers-reduced-motion ?

Lis la media query avec un hook useReducedMotion (Framer Motion en exporte un). Quand il retourne true, supprime le burst de particules, désactive le décalage magnétique et arrête les animations des anneaux pulsants en passant repeat à 0.

"use client";

import { useRef, useState, useCallback } from "react";
import { motion, useMotionValue, useSpring, AnimatePresence } from "framer-motion";
import { ArrowRight } from "lucide-react";

interface CtaMagneticBurstProps {
  title?: string;
  titleAccent?: string;
  description?: string;
  ctaLabel?: string;
  ctaUrl?: string;
}

const BURST_COUNT = 8;
const BURST_RADIUS = 60;
const SPRING_CFG = { stiffness: 180, damping: 14, mass: 0.08 };
const EASE = [0.16, 1, 0.3, 1] as const;

const burstParticles = Array.from({ length: BURST_COUNT }, (_, i) => {
  const angle = (i / BURST_COUNT) * Math.PI * 2;
  return { id: i, tx: Math.cos(angle) * BURST_RADIUS, ty: Math.sin(angle) * BURST_RADIUS };

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Autres variantes cta

Avis

Bouton magnétique React avec burst de particules, Framer