Cursor Trail
Dix points en chaîne qui suivent le curseur par interpolation : chaque point rattrape son prédécesseur à 30 % par frame, si bien que la queue ondule loin derrière — elle s'allonge dans les virages et se rétracte à l'arrêt.
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
Au démarrage, le script crée dix div (classe cursor-trail), les accroche à document.body et les stocke dans trailDots avec leurs coordonnées à (0, 0). Chaque point reçoit à la création une opacité figée — 1 − i × 0,1, de 1,0 à 0,1 — et une échelle fixe — scale(1 − i × 0,08), de 1,0 à 0,28. Ces valeurs ne changent plus. Tous restent en display: none jusqu'au mouseenter ; mouseleave les cache. La position du curseur est capturée dans trailX / trailY à chaque mousemove.
Toute la cinématique tient dans animateTrail(), lancée par requestAnimationFrame au démarrage et qui ne s'arrête jamais. À chaque frame, les dix points sont parcourus dans l'ordre. Le point 0 rejoint la position courante du curseur ; chaque suivant rejoint son prédécesseur. La formule est identique pour tous : dot.x += (prev.x − dot.x) × 0,3. Parcourir 30 % de l'écart restant par frame est une interpolation qui converge sans jamais dépasser la cible. Le retard se cumule de maillon en maillon : le dernier point n'a que (0,7)^9 ≈ 4 % de la réactivité de la tête.
Deux détails d'implémentation ont des conséquences directes. Les points sont accrochés à body, pas à la zone : leurs coordonnées (clientX / clientY) sont des positions dans la fenêtre, rien ne les clippe. Et la boucle rAF ne s'arrête pas : pendant que display: none les cache, les dix éléments continuent d'interpoler vers les dernières coordonnées connues — quand mouseenter les réaffiche, la traînée reprend sans à-coup. La position de chaque point est décalée de −5 px sur les deux axes pour centrer un point de 10 × 10 px.
Accessibilité
- prefers-reduced-motion : absent du code. La boucle rAF tourne sans condition et
cursor-activemasque le curseur natif quoi qu'il arrive. Conditionne le lancement d'animateTrail()et l'ajout des écouteurs àwindow.matchMedia('(prefers-reduced-motion: reduce)')inversée — les utilisateurs qui ont activé cette préférence ne doivent pas perdre leur curseur. - Sur écran tactile, aucun
mousemovene circule et la traînée est inerte. Mais untouchstartpeut déclencher unmouseentersynthétique sur certains navigateurs, activantcursor-activeet masquant le curseur sans traînée visible. Conditionne les écouteurs àmatchMedia('(hover: hover) and (pointer: fine)')pour bloquer cette émulation. - Plusieurs zones instanciées = autant de boucles rAF parallèles. Pour stopper la boucle hors survol, mémorise l'identifiant rAF, appelle
cancelAnimationFrame()surmouseleaveet relance surmouseenter. Pour plusieurs zones simultanées, une boucle globale unique gérant toutes les instances actives est plus propre. - Les dix points sont des
divvides dansbody, sans rôle ni texte. Ajoutearia-hidden="true"à chaque point pendant la boucle de création pour les exclure de l'arbre d'accessibilité et éviter qu'un lecteur d'écran ne les énumère dans la navigation.
Compatibilité navigateur
La limite est côté JavaScript : fonctions fléchées et template literal (pour l'attribut scale()). IE 11 s'arrête sur une erreur de syntaxe avant la création du premier point. Le CSS qui positionne les points (position: fixed, pointer-events: none) et masque le curseur (cursor: none) est acquis partout depuis 2012.
Sans JavaScript, aucun point n'est créé et aucune traînée n'apparaît — repli sain, l'effet n'emporte rien de fonctionnel. Sur mobile, sans <code>mousemove</code>, les points restent en <code>display: none</code> et le curseur système s'affiche normalement. Si tu dois 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 |
|---|---|---|
| Nombre de points (10 dans la boucle d'initialisation) | 10 | Longueur directe de la traînée. Chaque point est un div dans le DOM et un lerp par frame — reste sous 20. 4–5 = queue courte et serrée ; 15–18 = ondulation qui persiste longtemps après un virage. |
| Facteur d'interpolation (0,3 dans animateTrail) | 0,3 | Part du chemin parcourue par chaque point par frame. 0,1 = traînée très flottante, la queue persiste longtemps ; 0,5 = traînée tendue, la queue rattrape vite la tête. Commence par ce réglage avant de toucher au nombre de points. |
| Gradient d'opacité (coefficient 0,1 par rang) | 1 − i × 0,1 | Figé à la création, jamais mis à jour. 0,05 = fondu très progressif ; 0,15 = effacement brutal. Avec 10 points, ne dépasse pas 0,1 : au-delà, les derniers rangs sont invisibles dès la création. |
| Gradient de taille (coefficient 0,08 par rang) | scale(1 − i × 0,08) | Aussi figé à la création. 0,05 donne une queue à 55 % ; 0,12 donne le dernier point à 8 % — quasi invisible. Combine-le avec le gradient d'opacité pour moduler taille et lisibilité indépendamment. |
| Décalage de centrage (−5 dans le JS) | −5 px sur les deux axes | Position écrite comme dot.x − 5 et dot.y − 5 pour centrer un point de 10 × 10 px. Taille CSS à 8 px → décalage −4 ; à 16 px → décalage −8. Un décalage incorrect déporte toute la traînée du vrai pointeur. |
| Élément parent des points (document.body) | document.body | Les points utilisent clientX / clientY — coordonnées de fenêtre, correctes uniquement si le parent n'a pas de contexte de positionnement. Pour clipper la traînée à la zone, déplace l'appendChild vers la zone et bascule sur e.offsetX / e.offsetY avec overflow: hidden. |
FAQ
cancelAnimationFrame() sur mouseleave et relance sur mouseenter.clientX / clientY — des positions dans la fenêtre. Accrochés à body, ils se placent correctement sans calcul de décalage. Dans un parent avec son propre contexte de positionnement, l'origine change et tous les points dérivent. Pour les confiner à la zone, bascule sur e.offsetX / e.offsetY avec position: absolute dans un parent position: relative et overflow: hidden.cursor-trail en CSS : change border-radius pour passer du cercle au carré ; background pour une couleur pleine ou un dégradé radial ; box-shadow pour un halo. Si tu modifies la taille CSS, ajuste le décalage −5 en conséquence. Pour une couleur différente par rang, accède à trailDots[i].el pendant la boucle d'initialisation et pose la couleur directement sur l'élément.