Curseurs✨ Premium

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é.

JSInteractive

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

CTA principal d'un hero — landing SaaS — Magnetic Effect exemple 1

① CTA principal d'un hero — landing SaaS

QuandUne landing page mise tout sur un seul bouton (« Réserver une démo ») et veut le rendre impossible à manquer sans le faire clignoter.
PourquoiL'attraction s'amorce à 150 px du centre : le bouton vient à la rencontre du curseur avant d'être survolé, ce qui agrandit sa cible effective (loi de Fitts). Le plafond à 30 % garde le mouvement discret — du soin, pas du gadget.
RéglagesRayon 120 px et coefficient 0.2 pour rester crédible sur un bouton ; transition .25s ; pose l'écouteur sur le conteneur du hero, pas sur le bouton, pour garder l'amorce à distance.
Icônes sociales d'un footer — dock flottant — Magnetic Effect exemple 2

② Icônes sociales d'un footer — dock flottant

QuandUne rangée d'icônes de 40–48 px (GitHub, LinkedIn, X…) dans un footer ou une barre flottante — des cibles petites et proches les unes des autres.
PourquoiSur de petites cibles, l'aimantation corrige les gestes imprécis : l'icône se décale vers le curseur et le clic tombe juste. C'est le cas où un coefficient élevé se justifie — le déplacement absolu reste faible puisque l'élément est petit.
RéglagesRayon réduit à 70–80 px pour que deux icônes voisines ne se disputent jamais le curseur ; coefficient monté à 0.4 ; transition .2s ; une paire zone/cible par icône.
Cartes projet d'une grille portfolio — site d'agence — Magnetic Effect exemple 3

③ Cartes projet d'une grille portfolio — site d'agence

QuandUne grille de vignettes projet où chaque carte doit réagir à l'approche du curseur et inviter au clic, sans transformer la page en fête foraine.
PourquoiSur une grande surface, quelques pixels suffisent à créer une sensation de matière qui répond : la carte « respire » vers le visiteur, et le retour élastique du mouseleave lui fait reprendre sa place d'elle-même.
RéglagesCoefficient 0.12–0.15 — une grande carte doit bouger moins qu'un petit bouton ; rayon 200 px à l'échelle de la carte ; transition .4s pour un mouvement de masse ; espace ta grille en conséquence, une carte translatée peut chevaucher sa voisine.

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, mousemove ne 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-visible marqué (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.

Chrome 88+✓ Complet
Firefox 87+✓ Complet
Safari 14+✓ Complet
Edge 88+✓ Complet
Mobile iOS✓ Inerte (pas de curseur)
Android Chrome✓ Inerte (pas de curseur)

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) :

index.html — structure
🔒 Débloquer le code complet — à partir de 2,99 € le 1er mois

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éfautEffet
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

Le code cible deux 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.
Deux raisons. L'amorce à distance : posé sur .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.
Non — la fluidité ne vient pas du débit des 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.