Magnetic Effect
Un bloc attiré par le curseur dès qu'il entre dans son rayon de 150 px : distance recalculée à chaque mousemove, force linéaire plafonnée à 30 %, et une transition CSS de 300 ms qui rend le mouvement aimanté.
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'écouteur mousemove est posé sur la zone parente .cursor-zone (300 px de haut), pas sur la cible .magnetic-target elle-même — le détail qui fait tout : l'attraction s'amorce avant que le curseur touche le bloc. À chaque événement, le code relit la géométrie de la cible avec getBoundingClientRect(), en déduit son centre, puis mesure la distance euclidienne (Math.sqrt) entre ce centre et le pointeur (clientX / clientY).
Tout se joue ensuite sur un seuil de 150 px. Au-delà, la transform est remise à translate(0, 0). En deçà, une force linéaire est calculée : (150 − distance) / 150 — 0 au bord du rayon, 1 au contact du centre. Le déplacement appliqué vaut cette force multipliée par l'écart sur chaque axe et par un coefficient de 0.3 : la cible ne parcourt jamais plus de 30 % de la distance qui la sépare du curseur, elle tend vers lui sans le rattraper. Et comme getBoundingClientRect() est relu sur l'élément déjà déplacé, le calcul se réajuste à chaque événement : le mouvement s'auto-stabilise.
Le JavaScript n'anime rien : il écrit des valeurs brutes dans transform: translate(…) à chaque mousemove. C'est la règle CSS transition: transform .3s qui interpole entre deux écritures successives et fabrique l'inertie « aimantée » — ainsi que le retour élastique quand mouseleave remet la transform à zéro. Pas de requestAnimationFrame, pas de lerp maison : translate() est composité par le GPU, aucun reflow.
Accessibilité
- prefers-reduced-motion : absent du code. Les écouteurs écrivent des transforms quelle que soit la préférence système. Conditionne leur ajout à
window.matchMedia('(prefers-reduced-motion: reduce)')avant tout déploiement. - Au doigt,
mousemovene se déclenche pas : sur écran tactile, la cible reste simplement immobile — un repli sain. Ne fais donc jamais du déplacement magnétique le seul indice qu'un élément est cliquable : une partie de ton audience ne le verra jamais. - L'attraction est invisible au clavier : si tu fais de la cible un bouton ou un lien, la navigation au Tab n'en obtient aucun équivalent. Prévois un état
:focus-visiblemarqué (anneau, ombre) indépendant de l'effet. Bon point : la zone de clic suit la transform, l'élément déplacé reste cliquable là où il s'affiche. - La cible est un
<div>sans rôle ni texte porteur de sens — purement décorative telle quelle. Pour un usage interactif, pars d'un vrai<button>ou<a>: la transform ne modifie pas l'arbre d'accessibilité, les lecteurs d'écran n'annoncent rien du mouvement, la sémantique doit venir de l'élément lui-même.
Compatibilité navigateur
Côté CSS, transform: translate() et les transitions sont acquis partout depuis 2013. Le vrai plancher est côté JavaScript : le code utilise des fonctions fléchées et un template literal (ES2015) — IE 11 s'arrête sur une erreur de syntaxe avant le premier mousemove.
Repli intégré à la conception : sans JavaScript, sur moteur ES5 ou sur écran tactile, aucune transform n'est écrite et la cible reste centrée, entièrement fonctionnelle — le déplacement est purement additif, la mise en page n'en dépend jamais. S'il faut vraiment couvrir un moteur ES5, transpile le script : la logique n'a aucune autre dépendance.
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 |
|---|---|---|
| Rayon d'activation (150 dans le JS) | 150 px | La valeur 150 apparaît deux fois : dans la condition distance < 150 et dans la formule de force (150 - distance) / 150. Change-les ensemble, sinon la force ne s'annule plus au bord du rayon et l'entrée dans la zone produit un à-coup. |
| Coefficient d'attraction (0.3) | 0.3 | Multiplicateur final du translate : la part du chemin vers le curseur que la cible accepte de parcourir. 0.1 = frémissement discret ; 0.5 = le bloc colle presque au pointeur. Au-delà, la cible peut sortir de sa zone — l'overflow: hidden du conteneur la rognera. |
| Durée de la transition (transform .3s) | .3s | C'est elle qui lisse les écritures successives du JS — et qui règle la vitesse du retour au mouseleave. .15s = réponse nerveuse ; .5s = traînée visqueuse. |
| Courbe de la transition | ease (implicite) | Le raccourci transition: transform .3s ne précise pas de timing-function : c'est ease. Ajoute cubic-bezier(0.34, 1.56, 0.64, 1) pour que le retour dépasse légèrement sa position de repos avant de se poser — un vrai rebond d'aimant. |
| Dimensions de la cible (120 × 120) | 120 px × 120 px | La géométrie est relue à chaque événement via getBoundingClientRect() : change width et height librement, le centre est recalculé tout seul — contrairement au rayon, codé en dur dans le JS. |
| Zone d'écoute (.cursor-zone) | 300 px de haut | Le conteneur porte l'écouteur mousemove, pas la cible. Élargis-le pour que l'aimantation s'amorce de plus loin ; plus petit que le rayon de 150 px, il rend le seuil inutile — l'effet devient actif partout dans la zone. |
FAQ
id uniques via getElementById — une seule paire zone/cible. Passe sur des classes avec querySelectorAll et boucle en recréant le couple écouteur/cible pour chaque paire : le calcul étant relatif au centre de chaque cible, rien d'autre ne change. Si plusieurs cibles partagent la même zone (un dock d'icônes), calcule la distance de chacune dans le même écouteur et n'aimante que celles sous le seuil..magnetic-target, mousemove ne se déclencherait qu'au survol direct — l'attraction à 150 px disparaîtrait. La stabilité : la cible se déplace sous le curseur, l'écouter directement produirait des entrées/sorties en cascade. Le parent, lui, ne bouge pas : le flux d'événements reste continu.mousemove mais de transition: transform .3s, qui interpole entre deux écritures successives. Si le rendu semble haché, allonge la durée de transition avant d'envisager un requestAnimationFrame. Un lerp par frame ne se justifie que pour un suivi 1:1 permanent du curseur — c'est un autre effet.