BoutonsGratuit

Magnetic Add to Cart

Un bouton « Ajouter au panier » qui attire le curseur sur son passage, puis envoie l'article dans le panier d'un vol de particules — la micro-interaction qui rend l'achat satisfaisant à chaque clic.

Vanilla JSMicro-interactionE-commerce

Mis à jour le

0

Casque Studio Pro

149 €
Survolez ou cliquez la scène pour interagir

Cet effet est gratuit — la catégorie Boutons en contient 27 au total, dont 2 gratuits. Effect.Labs regroupe 840 effets vanilla. Découvrir la catégorie →

Exemples d'usage

0
Chaussures randonnée
Trailblazer GTX Mid
Réf. MV-TBZ-240 · Vert olive · EU 42
119 €
En stock · Livraison sous 24 h

① Fiche produit — boutique outdoor, ajout au panier

QuandPage produit d'une boutique spécialisée : une fiche centrée avec photo, nom, prix et le bouton « Ajouter au panier ». Le curseur est attiré dès l'approche, puis l'article part en vol jusqu'au panier de l'en-tête.
PourquoiLe magnétisme guide l'œil vers l'action principale sans qu'aucun texte ne la réclame. Le vol confirme instantanément que le produit a rejoint le panier — pas besoin d'un toast ni d'une alerte.
RéglagesRemplacez data-icon sur .atc-scene par l'emoji de votre produit. Positionnez .atc-cart-wrap dans l'en-tête de votre page : le script calcule sa position à chaque clic via getBoundingClientRect().
0
Sorties du mois — 3 livres
La Carte spirale
E. Morvan · Fiction
0
Avant le silence
L. Garnier · Thriller
0
Terres fertiles
P. Duval · Essai
0

② Liste de favoris — plateforme culturelle, carte produit

QuandGrille de résultats (livres, films, recettes…) : chaque carte expose un bouton « Ajouter aux favoris ». Plusieurs boutons coexistent dans la même scène, chacun envoyant son icône vers le compteur de favoris commun posé dans la barre supérieure.
PourquoiDans une grille dense, le magnétisme signale l'interaction disponible sans agrandir la carte. Le vol vers le compteur commun donne un repère spatial au résultat de l'action — l'utilisateur sait où ses favoris vont.
RéglagesPosez un .atc-cart-wrap partagé hors des cartes, dans l'en-tête. Chaque .atc-scene[data-atc] de la grille est indépendant ; le script trouve le .atc-cart à l'intérieur de sa propre scène, donc placez une copie légère du panier dans chaque carte ou utilisez un cart externe et recalculez cr manuellement.
0
4,9 · 2 400 avis
Bien manger,
chaque semaine
Des recettes de chef, des ingrédients frais livrés, prêts en 30 min. Première box offerte.

③ Landing page — bouton d'appel à l'action principal

QuandSection héro d'un service en ligne : un titre, une promesse courte et un seul bouton « Essayer gratuitement » ou « Commencer ». Le curseur magnétique intensifie l'attention sur la seule action que la page veut déclencher.
PourquoiSur un héro minimaliste, le magnétisme fait office de repère visuel en mouvement — subtil, sans animation intempestive. L'article qui rejoint une cible symbolique confirme que la démarche a été enregistrée.
RéglagesRemplacez data-icon par un symbole contextuel (📋, ✅, 🚀) et positionnez .atc-cart-wrap en haut à droite de la section. Le bouton CTA devient [data-atc-btn] ; adaptez son style avec les variables CSS du thème.

Comment ça marche

Le magnétisme repose sur pointermove : à chaque déplacement du curseur sur le bouton, le script calcule les écarts en X et en Y par rapport au centre du bouton (getBoundingClientRect()), puis applique transform: translate(mx × 0.22, my × 0.35). Les deux coefficients sont intentionnellement différents — l'attraction verticale (0,35) est plus forte que l'horizontale (0,22), ce qui fait pencher le bouton vers le curseur de façon naturelle. Sur pointerleave, le transform revient à vide : la transition CSS prend le relais pour un retour en douceur. L'ensemble est désactivé si prefers-reduced-motion: reduce est actif.

Au clic, le script calcule les positions du bouton et du panier par rapport au conteneur de scène (scene.getBoundingClientRect()). Un élément .atc-fly est créé à la position du bouton et animé via une boucle requestAnimationFrame sur une courbe de Bézier quadratique : point de départ au centre du bouton, point de contrôle levé à mi-chemin (cy = min(y0, y1) − 70), point d'arrivée au centre du panier. L'easing ease-in-out est appliqué à chaque paramètre de la courbe ; l'icône rétrécit en vol (scale de 1 à 0,45) sur 620 ms, puis l'élément est retiré du DOM.

Simultanément, 14 <span class="atc-particle"> sont injectés à la position du bouton — chacun reçoit un angle et une distance aléatoires via les variables CSS --dx et --dy de l'animation atc-burst, qui les projette en éventail avant de les faire disparaître en 650 ms. Le compteur ([data-atc-count]) est incrémenté et déclenche l'animation CSS atc-pop (agrandissement 1,6× puis retour). L'icône du panier (.atc-cart) déclenche atc-bump — une légère montée et rotation qui confirme la réception.

Accessibilité

  • Bouton natif : [data-atc-btn] est un <button type="button"> standard, atteignable au clavier (Tab), activable à l'Espace et à l'Entrée, et annoncé correctement par les lecteurs d'écran sans ARIA supplémentaire.
  • Mouvement réduit : sous prefers-reduced-motion: reduce, le magnétisme et le vol sont désactivés — seul le compteur est incrémenté. La classe CSS transition: none est appliquée au bouton par la feuille de style, et le script détecte la media query au chargement.
  • Compteur accessible : l'élément [data-atc-count] porte un aria-label (« Articles dans le panier ») mis à jour à chaque clic. Un lecteur d'écran annonce la valeur du compteur sans avoir besoin d'un aria-live supplémentaire si le bouton est focalisé.
  • Éléments décoratifs masqués : le panier (.atc-cart), l'icône en vol (.atc-fly) et les particules (.atc-particle) portent aria-hidden="true" ou sont créés sans rôle — ils ne parasitent pas la lecture d'écran.

Compatibilité navigateur

S'appuie sur PointerEvent (pointermove, pointerleave) disponible depuis Chrome 55, Firefox 59, Safari 13, requestAnimationFrame, getBoundingClientRect et les variables CSS (--dx, --dy). Aucune dépendance externe.

Chrome 55+✓ Complet (mesuré)
Firefox 59+✓ Complet (mesuré)
Safari 13+✓ Complet (mesuré)
Edge 79+✓ Complet (moteur Chromium)
Mobile iOS 13+✓ Complet — PointerEvent sur iOS (non mesuré)
Android Chrome 55+✓ Complet (moteur Chromium)

Sans JavaScript, le bouton s'affiche normalement et reste cliquable — aucune micro-interaction, mais le panier fonctionne. Sur mobile, le magnétisme est inactif (il n'y a pas de survol sans pointeur) ; le vol et le compteur fonctionnent normalement au toucher.

Le code

Copiez les trois blocs dans votre page. Aucune dépendance.

HTML
<div class="fx-0807-root">
  <div class="atc-scene" data-atc data-icon="🎧">
    <div class="atc-cart-wrap">
      <span class="atc-cart" aria-hidden="true">🛒</span>
      <span class="atc-count" data-atc-count aria-label="Articles dans le panier">0</span>
    </div>
    <div class="atc-product">
      <div class="atc-thumb" aria-hidden="true">🎧</div>
      <div class="atc-info">
        <h3>Casque Studio Pro</h3>
        <span class="atc-price">149 €</span>
        <div><button type="button" class="atc-btn" data-atc-btn>Ajouter au panier</button></div>
      </div>
    </div>
  </div>
</div>
CSS
.fx-0807-root,
.fx-0807-root *,
.fx-0807-root *::before,
.fx-0807-root *::after {
  box-sizing: border-box
}

.fx-0807-root {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 200px;
  position: relative;
  background: rgb(26, 26, 36);
  color: rgb(255, 255, 255);
  font-family: sans-serif;
}

.atc-scene {
  position: relative;
  width: 100%;
  min-height: 240px;
  border-radius: 14px;
  overflow: hidden;
  isolation: isolate;
  background: radial-gradient(circle at 70% 20%, #1c1330, #0b0a14 75%);
  padding: 22px;
  box-sizing: border-box;
}

.atc-cart {
  position: absolute;
  top: 16px;
  right: 18px;
  font-size: 26px;
  line-height: 1;
}

.atc-count {
  position: absolute;
  top: -8px;
  right: -10px;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  box-sizing: border-box;
  background: #ef4444;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  border-radius: 999px;
  display: grid;
  place-items: center;
}

.atc-cart-wrap {
  position: absolute;
  top: 16px;
  right: 18px;
}

.atc-product {
  display: flex;
  align-items: center;
  gap: 16px;
  max-width: 360px;
  margin-top: 34px;
}

.atc-thumb {
  width: 78px;
  height: 78px;
  border-radius: 14px;
  background: linear-gradient(135deg, #6366f1, #d946ef);
  display: grid;
  place-items: center;
  font-size: 38px;
  flex: none;
}

.atc-info h3 {
  margin: 0 0 2px;
  font-size: 16px;
  color: #fff;
}

.atc-info .atc-price {
  color: #a78bfa;
  font-weight: 700;
}

.atc-btn {
  margin-top: 14px;
  border: 0;
  cursor: pointer;
  background: linear-gradient(135deg, #f59e0b, #ef4444);
  color: #fff;
  font-weight: 700;
  font-size: 14px;
  padding: 11px 20px;
  border-radius: 10px;
  transition: transform .12s, box-shadow .2s;
  will-change: transform;
}

.atc-btn:hover {
  box-shadow: 0 8px 24px rgba(245, 158, 11, .35);
}

.atc-btn:active {
  transform: scale(.96);
}

.atc-particle {
  position: absolute;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #fbbf24;
  pointer-events: none;
  transform: translate(-50%, -50%);
  animation: atc-burst .6s ease-out forwards;
  z-index: 4;
}

.atc-fly {
  position: absolute;
  font-size: 30px;
  pointer-events: none;
  z-index: 5;
  transform: translate(-50%, -50%);
}

.atc-pop {
  animation: atc-pop .32s ease;
}

.atc-bump {
  animation: atc-bump .4s ease;
}

@media (prefers-reduced-motion: reduce) {
  .atc-btn {
    transition: none;
  }
}

@keyframes atc-burst {
  to {
    transform: translate(-50%, -50%) translate(var(--dx), var(--dy)) scale(.2);
    opacity: 0;
  }
}

@keyframes atc-pop {
  0% {
    transform: scale(1);
  }

  40% {
    transform: scale(1.6);
  }

  100% {
    transform: scale(1);
  }
}

@keyframes atc-bump {

  0%,
  100% {
    transform: translateY(0);
  }

  30% {
    transform: translateY(-7px) rotate(-8deg);
  }
}
JavaScript (fx-0807)
(function() {
  (function() {
    var reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

    document.querySelectorAll('[data-atc]').forEach(function(scene) {
      var btn = scene.querySelector('[data-atc-btn]');
      var cart = scene.querySelector('.atc-cart');
      var countEl = scene.querySelector('[data-atc-count]');
      var icon = scene.dataset.icon || '🛍️';
      var n = 0;

      if (!reduceMotion) {
        btn.addEventListener('pointermove', function(e) {
          var r = btn.getBoundingClientRect();
          var mx = e.clientX - (r.left + r.width / 2),
            my = e.clientY - (r.top + r.height / 2);
          btn.style.transform = 'translate(' + (mx * 0.22) + 'px,' + (my * 0.35) + 'px)';
        });
        btn.addEventListener('pointerleave', function() {
          btn.style.transform = '';
        });
      }

      function bump() {
        n++;
        countEl.textContent = n;
        countEl.classList.remove('atc-pop');
        void countEl.offsetWidth;
        countEl.classList.add('atc-pop');
        cart.classList.remove('atc-bump');
        void cart.offsetWidth;
        cart.classList.add('atc-bump');
      }

      btn.addEventListener('click', function() {
        if (reduceMotion) {
          bump();
          return;
        }
        var br = btn.getBoundingClientRect(),
          cr = cart.getBoundingClientRect(),
          sr = scene.getBoundingClientRect();
        var x0 = br.left + br.width / 2 - sr.left,
          y0 = br.top + br.height / 2 - sr.top;
        var x1 = cr.left + cr.width / 2 - sr.left,
          y1 = cr.top + cr.height / 2 - sr.top;

        for (var i = 0; i < 14; i++) {
          var p = document.createElement('span');
          p.className = 'atc-particle';
          p.style.left = x0 + 'px';
          p.style.top = y0 + 'px';
          var a = Math.random() * 6.2832,
            d = 24 + Math.random() * 46;
          p.style.setProperty('--dx', Math.cos(a) * d + 'px');
          p.style.setProperty('--dy', Math.sin(a) * d + 'px');
          scene.appendChild(p);
          (function(el) {
            setTimeout(function() {
              el.remove();
            }, 650);
          })(p);
        }

        var chip = document.createElement('span');
        chip.className = 'atc-fly';
        chip.textContent = icon;
        chip.style.left = x0 + 'px';
        chip.style.top = y0 + 'px';
        scene.appendChild(chip);
        var t0 = performance.now(),
          dur = 620,
          cx = (x0 + x1) / 2,
          cy = Math.min(y0, y1) - 70;
        (function anim(t) {
          var k = Math.min((t - t0) / dur, 1);
          var e = k < .5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2,
            m = 1 - e;
          var x = m * m * x0 + 2 * m * e * cx + e * e * x1;
          var y = m * m * y0 + 2 * m * e * cy + e * e * y1;
          chip.style.transform = 'translate(-50%,-50%) translate(' + (x - x0) + 'px,' + (y - y0) + 'px) scale(' + (1 - 0.55 * e) + ')';
          chip.style.opacity = String(1 - 0.25 * e);
          if (k < 1) requestAnimationFrame(anim);
          else {
            chip.remove();
            bump();
          }
        })(t0);
      });
    });
  })();
})();

Personnaliser

Options passées à l'API ou attributs data-* :

Option / propriétéDéfautEffet
Icône en vol — attribut data-icon sur .atc-scene 🛍️ Emoji affiché pendant le vol vers le panier. Remplacez-le par l'emoji du produit (🎧, 👟, 📚…) pour un retour visuel plus précis.
Force du magnétisme — facteurs 0.22 (X) et 0.35 (Y) dans le JS 0.22 / 0.35 Augmentez les deux pour une attraction plus forte ; descendez-les pour un effet quasi imperceptible. L'asymétrie X/Y donne le côté naturel — équilibrez-les pour une attraction isotrope.
Durée du vol — constante dur = 620 dans le JS 620 ms Réduisez à 400 ms pour un effet vif ; montez à 900 ms pour un vol lent et dramatique. La courbe de Bézier et l'easing sont recalculés à chaque clic, le changement est immédiat.
Nombre de particules — boucle for (var i = 0; i < 14; i++) 14 8 particules donnent un burst discret ; 24 un effet de fête plus prononcé. Au-delà de 30, les particules peuvent devenir lourdes sur mobile bas de gamme.
Couleur des particules — .atc-particle { background: #fbbf24 } dans le CSS #fbbf24 (ambre) Adaptez à la couleur de votre marque. Utilisez un dégradé ou plusieurs classes si vous voulez des particules multicolores (modifiez la création dans le JS).
Style du bouton — .atc-btn dans le CSS dégradé ambre → rouge, padding 11 × 20 px Remplacez le dégradé et les couleurs sans toucher au JS. L'effet magnétique s'adapte automatiquement à toute taille de bouton grâce à getBoundingClientRect().

FAQ

Le magnétisme fonctionne-t-il sur mobile ?

Non — pointermove sur mobile correspond au doigt qui glisse activement, pas à un survol. Le bouton ne se déplace pas au simple passage du doigt. C'est attendu : sur mobile, il n'y a pas de concept de « survol » avant le toucher. Le vol et le compteur fonctionnent normalement au toucher-clic.

Peut-on avoir plusieurs boutons dans la même scène qui envoient au même panier ?

Oui. Placez plusieurs [data-atc-btn] dans la même .atc-scene[data-atc] : chaque bouton calcule sa position par rapport à la scène et envoie son icône au même .atc-cart. L'icône en vol est celle définie par scene.dataset.icon — pour des icônes différentes par produit, lisez btn.dataset.icon dans le JS à la place.

Comment déclencher l'animation depuis un autre composant (case à cocher, liste de souhaits…) ?

Simulez un clic sur le bouton [data-atc-btn] depuis votre code : btn.dispatchEvent(new MouseEvent('click', { bubbles: true })). Le script utilise getBoundingClientRect() pour calculer la position de départ — pas besoin de fournir de coordonnées. Le vol partira du centre du bouton quelle que soit sa position dans la page.