Magnetic Elements
Cinq tuiles de 60 px attirées vers le pointeur dans un rayon de 120 px : jusqu'à 25 px de translation, un agrandissement jusqu'à ×1,15 et un halo indigo, recalculés à chaque mousemove et lissés par une transition transform de 300 ms. Aucun curseur personnalisé : ce sont les éléments qui bougent, la flèche système reste.
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
Le conteneur [data-magnetic-zone] (.magnetic-zone, flex centré, gap: 20px, padding: 20px, flex-wrap) contient cinq [data-magnetic-el] (.magnetic-item) : 60 × 60 px, border-radius: 16px, glyphe à 1.5rem, transition: transform .3s cubic-bezier(.23, 1, .32, 1), will-change: transform, cursor: pointer, et un dégradé différent par :nth-child (indigo, rose, cyan, ambre, vert). Le JS itère document.querySelectorAll('[data-magnetic-zone]') : plusieurs zones dans une page fonctionnent sans modification.
À chaque mousemove sur la zone, le script calcule la position locale du pointeur puis, pour chaque tuile, la distance d entre ce point et le centre de la tuile au repos — mesuré une seule fois puis mémorisé (voir plus bas). Si d < 120 : force = 25 × (1 − d / 120), angle atan2, puis transform: translate(cos θ × force, sin θ × force) scale(1 + .15 × (1 − d / 120)) et box-shadow: 0 0 (20 + force)px rgba(99,102,241, .3 + .4 × (1 − d / 120)) — tout en style inline. Sinon : translate(0, 0) scale(1) et box-shadow: none. mouseleave remet les cinq à zéro. Mesuré : à 119 px, translation 0,2 px, échelle 1,001, halo 20 px à 30 % ; à 60 px, 12,5 px, ×1,075, halo 32,5 px à 50 %.
Le halo n'est pas dans la liste des transitions (seule transform y est) : au passage de la frontière des 120 px, box-shadow saute de none à un flou de 20 px à 30 % d'un coup — un « pop » visible, pendant que la translation démarre en douceur. Et il est indigo pour les cinq tuiles, y compris la rose, l'ambre et la verte : la couleur du halo est codée en dur, indépendante du dégradé de la tuile.
Le centre de chaque tuile est relevé une seule fois, au repos : au premier mousemove, le script neutralise le transform de la tuile, lit sa boîte (getBoundingClientRect), mémorise dans t.__repos ses décalages par rapport à la zone (dx, dy) et sa taille, puis rétablit le transform. Aux événements suivants, le centre se déduit de la boîte courante de la zone et de ces décalages : la boîte déjà translatée n'est jamais relue, le vecteur ne s'inverse pas quand la tuile dépasse le pointeur, et la tuile se déplace d'une quantité qui ne dépend que de sa distance de repos, sans osciller. Les décalages étant relatifs à la zone, le défilement de la page ne fait pas dériver le centre. Ce que la mémorisation ne couvre pas : une disposition qui change après la première mesure (fenêtre redimensionnée, tuile ajoutée) — t.__repos garde les anciens décalages. Autre point : .magnetic-zone { height: 100% } dans un parent sans hauteur définie vaut auto — la zone active mesurée fait 100 px de haut (60 + 2 × 20) dans un aperçu de 300 px, et un pointeur 30 px au-dessus de la bande n'attire rien.
Accessibilité
- Contraste des glyphes : blanc à 24 px normal (grand texte, seuil 3:1) sur cyan
#06b6d4= 2,4:1, ambre#f59e0b= 2,2:1, émeraude#10b981= 2,5:1, turquoise#14b8a6= 2,5:1 — quatre des dix arrêts de dégradé échouent ; rose 3,5/3,7 et indigo 4,5/4,2 passent. Utilisez les nuances 600/700 :#0e7490 → #1d4ed8(5,4 et 6,7:1),#b45309 → #b91c1c(5,0 et 6,5:1),#047857 → #0f766e(5,5:1). Les glyphes décoratifs échappent à la règle : marquez-lesaria-hidden. cursor: pointerest posé sur desdivqui ne réagissent à aucun clic et n'ont ni rôle ni nom : la main promet une action qui n'existe pas. Soit les tuiles deviennent desbutton/a(l'effet marche à l'identique,[data-magnetic-el]est un simple sélecteur), soit retirezcursor: pointer.- Clavier : des
divne reçoivent pas le focus. Si vous en faites des boutons, l'attraction ne se déclenchera jamais au focus (le code n'écoute quemousemove) : donnez-leur un:focus-visibleexplicite (outline: 2px solid #c7d2fe; outline-offset: 4px), indépendant de l'effet. La zone cliquable suit la translation : une tuile déplacée de 25 px se clique à sa position visible. - prefers-reduced-motion absent : les tuiles glissent et grossissent sous le pointeur. Sous
@media (prefers-reduced-motion: reduce),.magnetic-item { transition: none }ne suffit pas — conditionnez plutôt l'écouteur :if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;avant d'attachermousemove. - Lecteurs d'écran : les symboles ♦ ★ ● ▲ ❖ sont annoncés (« losange noir », « étoile noire »…) sans aucun sens. Ajoutez
aria-hidden="true"sur chaque glyphe décoratif, ou remplacez-les par des icônes nommées (aria-label) si les tuiles deviennent des commandes. Tactile : un tap émule unmousemove— les tuiles proches sautent vers le point touché et y restent jusqu'aumouseleavesuivant.
Compatibilité navigateur
JavaScript ES5 (var, fonctions classiques, forEach sur NodeList), getBoundingClientRect, Math.atan2. CSS : transform, transition avec cubic-bezier, will-change, flex-wrap. Zéro dépendance — le plus rétro-compatible des curseurs du catalogue.
Sans JavaScript, cinq tuiles statiques correctement disposées : rien ne manque visuellement. Sans transitions CSS (navigateurs très anciens), les tuiles sautent d'une position à l'autre à chaque événement. NodeList.forEach est absent avant Chrome 51 / Firefox 50 / Safari 10 : le script s'arrête sur une erreur, tuiles statiques.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="magnetic-zone" data-magnetic-zone>
<div class="magnetic-item" data-magnetic-el>♦</div>
<div class="magnetic-item" data-magnetic-el>★</div>
<!-- … 5 au total, une par tuile … -->
</div>
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'attraction (JS — f = 120) |
120 px | Distance pointeur-centre à partir de laquelle une tuile réagit. 80 = seule la tuile survolée bouge ; 200 = toute la rangée ondule. |
Force maximale (JS — 25 * (1 - i / f)) |
25 px | Translation au contact. 12 = frémissement ; 40 = tuiles qui se chevauchent si l'écart est inférieur. |
Agrandissement (JS — 1 + .15 * (1 - i / f)) |
×1,15 | Échelle au contact. .3 pour un dock qui gonfle ; 0 pour une pure translation. |
Halo (JS — rgba(99,102,241, .3 + .4 × …), flou 20 + force) |
indigo, 20 à 45 px | Couleur et taille de l'ombre portée. Lisez une variable CSS par tuile (getComputedStyle(t).getPropertyValue('--glow')) pour un halo assorti au dégradé. |
Transition (CSS — transform .3s cubic-bezier(.23, 1, .32, 1)) |
300 ms, sortie élastique | Lissage du mouvement. Ajoutez box-shadow .3s pour supprimer le saut du halo au bord des 120 px ; .15s pour un suivi plus nerveux. |
Point de mesure (JS — t.__repos) |
position au repos, mesurée une fois | Centre relevé transform neutralisé, mémorisé en décalage par rapport à la zone : la tuile se pose sous le pointeur sans osciller et le défilement ne décale rien. Si la disposition change après coup (redimensionnement, tuile ajoutée), remettez t.__repos = null sur chaque tuile — la mesure suivante repart de zéro. |
Couleurs des tuiles (CSS — .magnetic-item:nth-child(n)) |
5 dégradés 500 | Un dégradé par position. Les nuances 600/700 (#4f46e5, #db2777, #0e7490, #b45309, #047857) donnent ≥ 4,6:1 au glyphe blanc. |
FAQ
mousemove sur la zone, pour chaque tuile : le transform en ligne est mis à none, la boîte lue par getBoundingClientRect(), ses décalages par rapport à la zone et sa taille stockés dans t.__repos, puis le transform rétabli. Les événements suivants ne relisent jamais la boîte translatée — c'est ce qui évite l'oscillation qu'entraînerait une mesure sur la tuile déjà déplacée (le vecteur s'inverserait dès que la tuile dépasse le pointeur). Les décalages étant relatifs à la zone, un défilement de page ne fausse rien. En revanche, si les tuiles se réorganisent après cette première mesure (fenêtre redimensionnée, flex-wrap qui passe à la ligne, tuile ajoutée), les décalages mémorisés sont périmés et l'attraction vise l'ancien emplacement : remettez t.__repos = null sur chaque tuile dans un écouteur resize, ou après toute modification du DOM.transform. À la frontière des 120 px, box-shadow passe sans étape de none à 0 0 20px rgba(99,102,241,.3) (mesuré à 119 px), et retombe à none en sortant. Ajoutez box-shadow .3s à la transition de .magnetic-item : le flou et l'opacité s'interpolent, et le halo naît en douceur. Le coût d'une box-shadow animée sur cinq tuiles est négligeable.[data-magnetic-el], n'importe quel élément convient ; gardez display: flex et le centrage pour le glyphe, et ajoutez un :focus-visible. Quant à la hauteur : .magnetic-zone { height: 100% } se résout en auto quand le parent n'a pas de hauteur définie (un min-height ne compte pas) — mesuré 100 px de haut dans un aperçu de 300 px, et l'attraction ne se déclenche que dans cette bande. Donnez au parent une hauteur, ou à la zone un min-height ou un padding vertical généreux.