Boutons✨ Premium

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.

JSMouseParticles

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

Ajouter au panier — fiche produit beauté ou mode — Particle Trail exemple 1

① Ajouter au panier — fiche produit beauté ou mode

QuandCarte produit d'une boutique en ligne : visuel, nom, prix et un bouton d'achat.
PourquoiLe dégradé rose et la traînée s'accordent à un univers cosmétique ou mode ; le geste de la main sur le bouton est récompensé sans retarder le clic, car aucune animation d'attente n'est ajoutée.
RéglagesPalette réduite à trois teintes de la marque, diamètre 3 à 8 px (6 * Math.random() + 3) pour un bouton étroit, seuil 40 ms.
Prendre ses places — landing d'événement ou de festival — Particle Trail exemple 2

② Prendre ses places — landing d'événement ou de festival

QuandHero d'un site événementiel avec dates, lieu et un unique bouton de billetterie.
PourquoiSur un grand bouton la traînée a de la place, et le overflow hidden la garde propre dans les coins arrondis : l'effet festif reste contenu, il ne déborde pas sur le titre.
RéglagesSeuil abaissé à 20 ms (≈ 50 points/s), diamètre 6 à 16 px, couleur d'accent de l'affiche ajoutée au tableau.
Révéler un code promo — carte d'offre de bienvenue — Particle Trail exemple 3

③ Révéler un code promo — carte d'offre de bienvenue

QuandPop-in ou carte « −20 % sur votre première commande » avec un bouton qui révèle le code.
PourquoiUn bouton de récompense appelle un retour ludique : les points qui montent et s'éteignent évoquent des confettis, sans bibliothèque. Le JS reste tel quel, seul le style du bouton change.
RéglagesDurée 800 → 1200 ms (setTimeout et particleFade ensemble), montée translateY(-32px), blanc retiré du tableau pour une traînée monochrome.

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 #ec4899 et 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é, ajoutez r.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: none sans 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.

Chrome 49+✓ Complet
Firefox 45+✓ Complet
Safari 10+✓ Complet
Edge 79+✓ Complet
Mobile iOS✓ Statique (pas de survol)
Android Chrome✓ Statique (pas de survol)

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

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

Le débit est borné par le seuil de 30 ms et la durée de vie de 800 ms : au plus 27 spans simultanés par bouton, chacun animé sur 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.
Oui : un <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.