Magnetic Cursor
Un bouton qui se penche vers ton curseur tant que tu le survoles : le JS mesure l'écart au centre, l'écrit dans deux variables CSS, et le :hover décide quand la translation s'applique.
Vous aurez accès à la démo interactive avec un compte gratuit.
Cet effet fait partie d'Effect.Labs — 811 effets vanilla, une partie gratuite, une partie premium. Curseurs comprend 26 effets, dont 1 gratuits. Découvrir la catégorie →
3 exemples d'usage



Comment ça marche
L'effet repose sur une séparation stricte des rôles. Le bouton est enveloppé dans un conteneur .magnetic-area, et c'est ce conteneur — pas le bouton — qui bouge. Côté CSS, la règle clé tient en une ligne : .magnetic-area:hover reçoit transform: translate(var(--magnetic-x, 0), var(--magnetic-y, 0)). La translation est pilotée par deux variables CSS avec un repli à 0, et elle n'est déclarée que sous :hover : hors survol, aucune transformation n'existe. Une transition: transform .3s sur le conteneur lisse à la fois la poursuite et le retour au repos.
Côté JavaScript, l'écouteur mousemove est posé sur le conteneur lui-même, pas sur la zone qui l'entoure. À chaque mouvement, getBoundingClientRect() donne la position réelle de l'élément ; le script en déduit l'écart du pointeur au centre (clientX - rect.left - rect.width / 2, idem en Y), le multiplie par 0,3 et écrit le résultat dans --magnetic-x et --magnetic-y via style.setProperty(). Le JS ne touche jamais à transform : il fournit des nombres, le CSS décide de leur application. Un second écouteur, mouseleave, remet les deux variables à 0px. Le tout tient en une quinzaine de lignes, sans requestAnimationFrame ni bibliothèque.
Cette architecture produit un comportement émergent : comme getBoundingClientRect() mesure la position déjà transformée, chaque mousemove calcule l'écart depuis l'endroit où l'élément se trouve à cet instant. La traction s'auto-amortit : l'élément converge sous le pointeur au lieu d'osciller, et se stabilise vers 23 % de l'écart initial (0,3 / 1,3), et non les 30 % du coefficient. Autre conséquence : sans JavaScript, les variables ne sont jamais écrites, le repli 0 s'applique et le bouton reste pleinement fonctionnel — l'effet est une amélioration progressive, pas une dépendance.
Accessibilité
- prefers-reduced-motion : absent du code. Ajoutez
@media (prefers-reduced-motion: reduce) { .magnetic-area:hover { transform: none; } }— une seule règle neutralise l'effet, puisque toute la translation passe par ce sélecteur. - L'effet est 100 % souris :
mousemovene se déclenche ni au clavier ni au tactile. Le<button>natif reste focusable et activable à Entrée/Espace, mais aucun style:focus-visiblen'est défini — ajoutez-en un, les utilisateurs clavier ne verront jamais l'inclinaison magnétique. - Sur pointeur grossier (tactile), aucun
mousemovene circule en usage normal : l'effet est inerte, le bouton se comporte normalement. Pour bloquer lesmousemoveémulés au tap par certains navigateurs, conditionnez l'écouteur àmatchMedia('(hover: hover) and (pointer: fine)'). - Pour les lecteurs d'écran, rien ne change : le déplacement est purement visuel, le
<button>conserve son nom accessible et sa place dans l'arbre d'accessibilité. Ne portez aucune information par le mouvement lui-même — il est invisible pour une partie des utilisateurs.
Compatibilité navigateur
Repose sur les variables CSS (var() écrites via setProperty()), les transitions et getBoundingClientRect(). Le facteur limitant est le support des custom properties — acquis partout aujourd'hui, absent d'IE 11.
La dégradation est naturelle : sans variables CSS (IE 11), <code>translate(var(…))</code> est invalide, le navigateur ignore la déclaration et le bouton s'affiche statique mais pleinement cliquable. Sans JavaScript, les variables ne sont jamais écrites et le repli 0 de <code>var(--magnetic-x, 0)</code> garantit une translation nulle. Aucun code de secours à écrire.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
HTML + CSS + JS complet, prêt à coller — avec des centaines d'effets premium.
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
| Coefficient d'attraction (x * 0.3 / y * 0.3 dans le JS) | 0.3 | Les deux multiplications fixent l'amplitude : la valeur écrite dans les variables vaut 30 % de l'écart au centre. 0,1 = frémissement à peine perceptible ; 0,5 = poursuite marquée. Modifiez les deux occurrences ensemble pour garder un mouvement isotrope. |
| transition: transform .3s (sur .magnetic-area) | .3s | Lisse chaque nouvelle valeur des variables et anime le retour au repos après mouseleave. 0,15 s = réponse vive, collée au curseur ; 0,5 s = traîne visible, le bouton semble lourd. C'est l'unique réglage de vitesse de l'effet. |
| Courbe du retour (timing-function) | ease (implicite) | Le raccourci laisse la courbe ease par défaut. Passez à cubic-bezier(0.34, 1.56, 0.64, 1) pour un retour élastique qui dépasse légèrement la position de repos — la sensation « lâcher d'aimant » typique de ce genre d'interaction. |
| Le garde :hover (.magnetic-area:hover) | translation appliquée uniquement au survol | Déclarer le transform sous :hover est une sécurité : hors survol, aucune translation ne peut subsister. Déplacez la déclaration sur .magnetic-area seul si vous pilotez les variables autrement — le retour au repos ne dépendra alors que du mouseleave JS. |
| Coefficients dissymétriques X / Y | 0.3 sur les deux axes | Rien n'impose le même facteur sur les deux axes : y * 0.15 avec x * 0.3 contraint la verticale et produit un glissement latéral — pertinent pour un bouton large et bas, où l'écart horizontal domine largement l'écart vertical. |
| Le contenu de l'enveloppe (.magnetic-btn) | un <button> stylé en dégradé | .magnetic-area porte l'écouteur, la transition et la translation — le bouton n'est qu'un passager. Remplacez-le par un lien, une icône SVG ou une carte entière : tant que l'élément reste dans l'enveloppe, la mécanique fonctionne sans toucher au JavaScript. |
FAQ
:hover. L'un attire de loin, l'autre accompagne au contact. Et côté code : le premier écrit le transform inline en JS ; ici le JS n'écrit que deux variables, le CSS garde la main.getElementById — une seule instance. Pour en équiper plusieurs, passez par la classe : parcourez document.querySelectorAll('.magnetic-area') et attachez la paire mousemove/mouseleave à chaque élément de la boucle. Les variables étant écrites sur le style inline de chaque enveloppe, les instances sont indépendantes.mousemove ne circule, les variables restent au repli 0 et le bouton se comporte normalement. Inutile de désactiver quoi que ce soit. Pour distinguer stylet ou trackpad, la media query (hover: hover) and (pointer: fine) reste le bon critère.