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.
Mis à jour le
Casque Studio Pro
149 €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
chaque semaine
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 CSStransition: noneest 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 unaria-label(« Articles dans le panier ») mis à jour à chaque clic. Un lecteur d'écran annonce la valeur du compteur sans avoir besoin d'unaria-livesupplé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) portentaria-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.
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.
<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>
.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);
}
}
(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éfaut | Effet |
|---|---|---|
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.