Trail Particles
Chaque mousemove lâche deux points violets de 3 à 9 px soumis à une gravité de 0,05 px par image : ils dérivent, retombent, rétrécissent et s'éteignent en 0,55 à 1,1 s — une boucle requestAnimationFrame qui détruit et recrée tous les points à chaque image.
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 script parcourt chaque [data-trail-particles] et lui pose un écouteur mousemove sans limiteur de débit : à chaque événement, deux particules sont poussées dans un tableau (for (f = 0; f < 2; f++)). Chacune naît aux coordonnées du pointeur relatives à la zone (getBoundingClientRect) avec une vitesse horizontale 3 × (Math.random() − .5), soit −1,5 à +1,5 px par image, une vitesse verticale 3 × (Math.random() − .5) − 1, soit −2,5 à +0,5 px par image (le biais de −1 la fait partir vers le haut), une vie life = 1, une décroissance decay = .015 + .015 × Math.random(), un diamètre 3 + 6 × Math.random() (3 à 9 px) et une couleur tirée parmi six violets et indigos (#a78bfa, #6366f1, #8b5cf6, #c084fc, #e879f9, #818cf8).
La boucle l() tourne en requestAnimationFrame tant que le tableau n'est pas vide (un drapeau o évite de la lancer deux fois). À chaque image elle applique la physique : x += vx, y += vy, puis vy += .05 (gravité), vx *= .99 (frottement) et life −= decay. Avec une décroissance de 0,015 à 0,03 par image, une particule vit 33 à 67 images, soit 0,55 à 1,1 s à 60 images par seconde. Sur un trajet d'une seconde mesuré dans Chromium, 18 particules coexistaient au plus.
Le rendu est refait de zéro à chaque image : tous les .trail-particle sont retirés (querySelectorAll(...).forEach(e => e.remove())), puis un div neuf est créé par particule vivante — left/top en px, width/height = size × life (le point rétrécit en mourant), opacity = life, background de sa couleur et un halo box-shadow: 0 0 2·size px de la même couleur. Mesuré : aucun nœud ne survit d'une image à la suivante. Le CSS compagnon .trail-particle (position: absolute, border-radius: 50%, pointer-events: none) était absent du code vendu jusqu'au 5 septembre 2026 : sans lui, les div restaient en flux, invisibles.
La zone .trail-zone fait 100% de large sur 300px de haut (dégradé radial #1a1a3e → #0a0a1a, cursor: crosshair), en position: relative pour servir de repère aux left/top. Elle est en overflow: hidden : les particules qui retombent sont découpées au bord — sans cette découpe, une particule émise à 6 px du bord bas retombait jusqu'à 121 px sous la zone (mesuré), par-dessus le contenu voisin. Les coordonnées placent le coin haut-gauche du point sous le pointeur, pas son centre. Enfin l'écouteur mouseleave est une fonction vide : rien n'est nettoyé à la sortie, les points meurent d'eux-mêmes.
Accessibilité
- prefers-reduced-motion absent du code : la traînée se produit quelle que soit la préférence système. Entourez l'ajout de l'écouteur d'un
if (!matchMedia('(prefers-reduced-motion: reduce)').matches)— la seule règle CSS.trail-particle { display: none }cacherait les points mais laisserait la boucle calculer et recréer des div pour rien. - L'effet est exclusivement souris : aucun
mousemoveau clavier, et au tactile le navigateur n'en émet au plus qu'un au toucher (deux points, aussitôt éteints). La zone est undivnon focalisable qui ne porte aucune information : gardez-la décorative, ne placez jamais de consigne ou de contenu que seule la traînée révélerait. - Contraste : le texte d'invite fourni,
rgba(255,255,255,.15)en 12,8 px, mesure 1,5:1 sur le fond — lisible par personne. S'il doit être lu, passez àrgba(255,255,255,.5)(5,3:1). Les particules sont non textuelles : la moins contrastée,#6366f1, reste à 4,4:1 sur#0a0a1a, au-dessus du seuil de 3:1 des éléments graphiques. - Lecteurs d'écran : les div injectés et retirés jusqu'à 60 fois par seconde sont vides, donc muets, mais ils font battre l'arbre d'accessibilité. Posez
aria-hidden="true"sur la zone[data-trail-particles]pour que tout ce qui y naît soit ignoré d'un bloc. Le pointeur natif reste visible (cursor: crosshair, pascursor: none) : aucun repère n'est retiré à l'utilisateur. - Intégration : la zone doit garder
position: relativeet une hauteur explicite — avecheight: 100%dans un parent sans hauteur, elle rendait 0 px et la souris n'y entrait jamais ; le code vendu fixe300px. L'overflow: hiddende la zone découpe au bord les points qui retombent : conservez-le si des éléments sont placés sous la zone, et ne passez àvisibleque pour laisser les points déborder sur un fond vide. Aucune remise à zéro n'est nécessaire à la sortie : la boucle s'arrête seule quand la dernière particule meurt.
Compatibilité navigateur
Code ES5 (var, function, aucune fonction fléchée) : il ne requiert que requestAnimationFrame, getBoundingClientRect, box-shadow et border-radius. Zéro dépendance.
Sans JavaScript, il reste une zone au dégradé radial et son texte d'invite : rien ne bouge, rien ne manque au contenu. Sans box-shadow (navigateurs très anciens), les points sont nets, sans halo.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="trail-zone" data-trail-particles>
<div style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none;color:rgba(255,255,255,0.15);font-size:0.8rem;">Déplacez le curseur ici</div>
</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 |
|---|---|---|
Particules par événement (JS — f < 2) |
2 | Nombre de points créés à chaque mousemove. Il n'y a aucun limiteur de débit : à 120 événements par seconde, 2 donne 240 points par seconde. 1 allège de moitié ; au-delà de 3, ajoutez un seuil temporel comme dans Particle Trail. |
Gravité (JS — e.vy += .05) |
.05 px/image² | Accélération verticale ajoutée à chaque image. 0 : les points flottent et dérivent ; .15 : ils tombent lourdement. Négative, ils s'envolent. |
Frottement (JS — e.vx *= .99) |
.99 | Multiplie la vitesse horizontale à chaque image. .9 freine net les points, 1 les laisse filer en ligne droite. Ne s'applique pas à la verticale. |
Décroissance (JS — decay: .015 + .015 × Math.random()) |
.015 à .03 par image | Détermine la durée de vie : 1 / decay images, soit 0,55 à 1,1 s à 60 images/s. Doublez les deux termes pour une traînée deux fois plus courte. La taille et l'opacité suivent life, donc la décroissance règle aussi la vitesse du rétrécissement. |
Diamètre (JS — size: 3 + 6 × Math.random()) |
3 à 9 px | Taille de naissance ; le point mesure ensuite size × life. Le halo box-shadow vaut deux fois le diamètre : agrandir les points agrandit la lueur. |
Palette (JS — tableau a) |
#a78bfa, #6366f1, #8b5cf6, #c084fc, #e879f9, #818cf8 | Couleur tirée au hasard par point, appliquée au fond et au halo. Une seule entrée = traînée unie ; vérifiez le contraste sur votre fond (3:1 pour un élément graphique). |
Vitesse initiale (JS — 3 × (Math.random() − .5) et − 1) |
±1,5 px/image, biais vertical −1 | Le facteur 3 règle la dispersion, le − 1 la poussée initiale vers le haut. − 3 fait jaillir les points comme une fontaine ; 0 les laisse tomber depuis le pointeur. |
FAQ
mousemove sans limiteur, et surtout le rendu, qui détruit et recrée chaque div à chaque image (mesuré : aucun nœud conservé entre deux images). Avec 18 points vivants, c'est 18 suppressions et 18 créations 60 fois par seconde. Deux corrections simples : ajouter un seuil temporel (if (Date.now() − dernier < 30) return) comme dans Particle Trail, et réutiliser les nœuds — garder el dans l'objet particule et n'écrire que style.left/top/width/height/opacity. La boucle, elle, s'arrête d'elle-même dès que le tableau est vide : au repos, l'effet ne consomme rien..trail-zone est en overflow: hidden, et les points que la gravité fait retomber sont découpés au bord bas — sans cette découpe, une émission à 6 px du bord descendait jusqu'à 120 px sous la zone (mesuré). Si la zone a des coins arrondis, le découpage les respecte. Pour laisser les points déborder — sur un hero plein écran, par exemple — passez la zone en overflow: visible : les particules étant en pointer-events: none, elles n'interceptent rien, mais elles se dessinent par-dessus les éléments suivants.querySelectorAll('[data-trail-particles]') et donne à chacune son tableau et sa boucle. Toute la page est possible en posant l'attribut sur un conteneur en position: relative qui l'englobe — mais les left/top sont relatifs à ce conteneur, pas à la fenêtre : s'il défile, les points nés en haut restent en haut. Pour une traînée fixe à l'écran, il faudrait passer les particules en position: fixed et utiliser clientX/clientY sans soustraire le rectangle de la zone.