Particle Trail
Chaque mouvement du curseur sur le bouton dépose un point coloré (4 à 12 px) qui monte et s'éteint en 800 ms — une traînée contenue dans le bouton.
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. Boutons comprend 27 effets, dont 2 gratuits. Découvrir la catégorie →
3 exemples d'usage



Comment ça marche
Chaque élément [data-particle-trail] reçoit un écouteur mousemove avec un limiteur de débit : Date.now() est comparé à la dernière émission, et tout événement survenant moins de 30 ms après le précédent est ignoré (if (o − t < 30) return). Le débit plafonne donc à environ 33 particules par seconde, quelle que soit la fréquence de la souris.
Pour chaque événement retenu, le script crée un <span class="particle"> : diamètre d = 4 + 8 × Math.random() (4 à 12 px), couleur tirée dans un tableau de cinq valeurs (#ec4899, #f43f5e, #fb7185, #fda4af, #ffffff), position left/top égale aux coordonnées du curseur relatives au bouton (getBoundingClientRect), et un box-shadow: 0 0 d px de la même couleur pour la lueur. Le span est ajouté dans le bouton.
Côté CSS, .particle est en position: absolute (le bouton est position: relative), rond (border-radius: 50%), en pointer-events: none pour ne jamais intercepter le clic, et animé par particleFade (.8s ease-out forwards) : opacité 1 → 0 et scale(1) → scale(0) translateY(-20px), le point rétrécit en montant de 20 px. Le overflow: hidden de .demo-btn découpe la traînée aux coins arrondis du bouton.
Nettoyage : un setTimeout(() => r.remove(), 800) par particule, calé sur la durée de l'animation — le DOM ne contient jamais plus d'une trentaine de spans par bouton (33/s × 0,8 s). Le soulèvement au survol est indépendant du JS : .btn-particle-trail:hover applique translateY(-2px) et une ombre rose 0 10px 30px rgba(236,72,153,.4) via transition: .3s.
Accessibilité
- prefers-reduced-motion absent du code : particules et soulèvement s'animent quelle que soit la préférence système. Ajoutez
@media (prefers-reduced-motion: reduce) { .particle { display: none } .btn-particle-trail { transition: none } }— le JS continue de créer des spans mais ils ne sont plus rendus — ou conditionnez l'écouteur àmatchMedia('(prefers-reduced-motion: reduce)').matches. - L'effet est exclusivement souris : aucune particule au toucher ni au clavier. Le bouton reste un
<button>natif, focusable et activable à Entrée/Espace ; la traînée ne doit porter aucune information. - Contraste insuffisant dans la version fournie : texte blanc sur le dégradé rose → framboise, soit 3,5:1 sur
#ec4899et 3,7:1 sur#f43f5e, sous le seuil AA de 4,5:1 pour un texte de 16 px. Passez à#db2777 → #e11d48(4,6:1 et 4,7:1) ou à un libellé de 18 px gras (seuil 3:1). - Lecteurs d'écran : les particules sont des
<span>vides insérés dans le<button>— sans texte, ils ne changent pas le nom accessible. Par sécurité, ajoutezr.setAttribute('aria-hidden', 'true')à la création pour que l'insertion répétée reste invisible à toutes les technologies d'assistance. - Le code écrit
border: nonesans toucher àoutline: l'anneau de focus natif reste. Cible tactile ≈ 52 px de haut (padding 16 px + texte 16 px), au-dessus du minimum de 44 px.
Compatibilité navigateur
Requiert ES2015 (fonctions fléchées, gabarits de chaîne), getBoundingClientRect, Date.now et une animation CSS sur opacity/transform — tous les navigateurs depuis 2016. Zéro dépendance.
Sans JavaScript, le bouton garde son dégradé et son soulèvement CSS au survol ; seules les particules disparaissent. Sans animations CSS (navigateurs très anciens), les points apparaissent nets puis sont retirés d'un coup à 800 ms.
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 |
|---|---|---|
Seuil 30 ms (JS — if (o - t < 30) return) |
30 | Débit maximal de particules (1000 / 30 ≈ 33 par seconde). 15 ms double la densité ; 60 ms donne des points espacés, plus lisibles. |
Diamètre 8 * Math.random() + 4 (JS) |
4 à 12 px | Plage de taille des points. La lueur (box-shadow) suit le diamètre : agrandir les points agrandit le halo. |
Tableau de couleurs (JS — f = [...]) |
#ec4899, #f43f5e, #fb7185, #fda4af, #ffffff | Palette tirée au hasard à chaque point. Une seule entrée = traînée unie ; gardez du blanc pour des étincelles. |
Durée de vie (JS — setTimeout 800 + CSS — particleFade .8s) |
800 ms / .8s | Les deux valeurs doivent rester égales : le setTimeout retire le span, l'animation le fait disparaître. Si l'animation est plus longue, le point est coupé net. |
Trajectoire (CSS — @keyframes particleFade, 100 %) |
scale(0) translateY(-20px) | Fin de course de chaque point. translateY(20px) fait tomber les points ; scale(1.6) sans translation donne des bulles qui éclatent. |
| overflow (CSS — .demo-btn) | hidden | Passez à visible pour que la traînée sorte du bouton — prévoyez alors de la marge autour, car les points ne sont plus découpés. |
| Survol (CSS — .btn-particle-trail:hover) | translateY(-2px) + ombre 0 10px 30px rgba(236,72,153,.4) | Soulèvement indépendant du JS. Supprimez la règle pour un bouton immobile : seule la traînée subsiste. |
FAQ
opacity et transform — deux propriétés composées par le GPU. Le vrai coût est la création DOM à chaque événement retenu ; montez le seuil à 50–60 ms si vous avez des dizaines de boutons sur la même page. Sur mobile rien n'est créé, faute de mousemove.<span> est un contenu phrasé autorisé dans un <button>. Ce qui n'y est pas autorisé, ce sont les éléments interactifs (lien, bouton). Si votre bouton est en réalité un <a>, le code fonctionne à l'identique — la seule contrainte est position: relative sur l'élément pour que les left/top des points aient un repère.left/top placent le coin haut-gauche du span sous le curseur, pas son centre : le point déborde de son rayon vers le bas et la droite (2 à 6 px). Pour le centrer, ajoutez translate(-50%, -50%) au transform des deux étapes de particleFade, ou soustrayez d / 2 aux coordonnées dans le JS.