Cursor Ripple Field
Au plus un anneau tous les 120 ms : un div de 2 px de bordure, teinte tirée entre 220° et 280°, qui grandit de 0 à 150 px en 1 s — largeur et scale animés ensemble, d'où un départ lent — pendant que son opacité s'éteint, puis se retire seul à animationend.
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
Chaque [data-ripple-field] reçoit un écouteur mousemove gardé par un verrou temporel : un booléen t passe à vrai à la première émission et un setTimeout de 120 ms le relâche. Tout mouvement pendant ce délai est ignoré, quelle que soit la fréquence de la souris : le débit plafonne à 1000 / 120 ≈ 8 anneaux par seconde. Mesuré dans Chromium : 9 anneaux créés pour 1,29 s de mouvement continu, 7 présents au plus au même instant.
Pour chaque émission retenue, le script crée un div.alchemy-cursor-ripple, le place en left/top aux coordonnées du pointeur relatives à la zone (getBoundingClientRect) et lui donne une couleur de bordure hsla(220 + 60 × Math.random(), 70%, 65%, .6) — une teinte entre le bleu (220°) et le violet (280°), à 60 % d'opacité. Le div est ajouté dans la zone et s'auto-détruit : un écouteur animationend appelle remove(). Il n'y a ni boucle requestAnimationFrame ni minuterie de nettoyage : la fin de l'animation CSS est la seule horloge.
Le mouvement est entièrement en CSS : .alchemy-cursor-ripple est position: absolute, border: 2px solid, rond, pointer-events: none, et joue cursorRippleExpand 1s ease-out forwards. Les images clés animent deux grandeurs à la fois : width/height de 0 à 150 px et transform: translate(−50%, −50%) scale(0 → 1). Le diamètre visible est leur produit : mesuré, 1 px à 50 ms, 14 px à 200 ms, 70 px à 500 ms, 150 px à 1 s — l'anneau part très lentement puis s'ouvre vite, alors qu'une seule grandeur en ease-out aurait fait l'inverse. L'opacité descend de 1 à 0 sur la même courbe (0,69 à 200 ms, 0,32 à 500 ms). La translate(−50%, −50%) centre l'anneau sur le point d'émission.
La zone .ripple-field-zone fait 100% × 300px, en dégradé linear-gradient(135deg, #0f0f2e, #1a1a3e), cursor: pointer, position: relative (repère des left/top) et overflow: hidden, qui découpe les anneaux nés près des bords. Le CSS vendu se limite à cette zone, à .alchemy-cursor-ripple et à ses images clés cursorRippleExpand : rien qui vienne d'un autre effet.
Accessibilité
- prefers-reduced-motion absent du code : les anneaux s'ouvrent quelle que soit la préférence système. Ajoutez
@media (prefers-reduced-motion: reduce) { .alchemy-cursor-ripple { animation: none; display: none } }— attention, sans animation l'événementanimationendne vient jamais et les div ne seraient plus retirés, d'où ledisplay: none; mieux : conditionnez l'écouteur àmatchMedia('(prefers-reduced-motion: reduce)').matchespour ne rien créer. - L'effet est exclusivement souris : aucun anneau au clavier, et au tactile au plus un par toucher (un seul
mousemoveémulé). La zone est undivnon focalisable ;cursor: pointerpromet un clic qui n'existe pas dans le code — remplacez-le pardefaultsi la zone n'est pas cliquable, ou branchez une vraie action. - Contraste : les anneaux sont des éléments graphiques décoratifs. À 60 % d'opacité sur le dégradé, la teinte la moins visible mesure 2,3:1 et la plus visible 2,9:1, sous le seuil de 3:1 des composants d'interface (WCAG 1.4.11) — acceptable pour un ornement, pas si l'anneau doit signaler quelque chose : passez l'alpha à
.9(≥ 3,5:1). Le texte d'invite fourni,rgba(255,255,255,.15), est à 1,5:1 : remontez-le à.5(5,3:1) s'il doit être lu. - Lecteurs d'écran : les div sont vides, donc muets, mais jusqu'à 8 insertions et retraits par seconde agitent l'arbre d'accessibilité. Posez
aria-hidden="true"sur la zone[data-ripple-field]. Le pointeur natif reste affiché (aucuncursor: none) : rien n'est retiré à l'utilisateur. - Intégration : la zone doit garder
position: relative(sinon les anneaux se placent par rapport à l'ancêtre positionné le plus proche) et une hauteur explicite — le code vendu fixe300px, une valeur en%dans un parent sans hauteur rend 0 px. Conservezoverflow: hidden, qui découpe les anneaux de 150 px nés au bord. Aucune remise à zéro à la sortie : chaque anneau se retire seul au bout d'une seconde.
Compatibilité navigateur
Code ES5 (var, function) : il requiert getBoundingClientRect, setTimeout, l'événement animationend, une animation CSS sur width/height, transform et opacity, et les couleurs hsla(). Zéro dépendance.
Sans JavaScript, la zone garde son dégradé et son texte d'invite : aucun anneau, rien de cassé. Sans animations CSS (navigateurs très anciens), un anneau serait créé à 0 px, invisible, et jamais retiré faute d'animationend — un div vide par émission, sans conséquence visible.
Le code
Structure HTML à coller dans votre page (CSS + JS disponibles avec un compte premium) :
<div class="ripple-field-zone" data-ripple-field>
<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 pour créer des ondulations</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 |
|---|---|---|
Verrou (JS — setTimeout(..., 120)) |
120 ms | Intervalle minimal entre deux anneaux, donc débit maximal (1000 / 120 ≈ 8 par seconde). 60 double la densité ; 250 donne des anneaux isolés, lisibles un à un. Le nombre d'anneaux simultanés vaut durée / verrou. |
Teinte (JS — 60 × Math.random() + 220) |
220° à 280° (bleu → violet) | Plage de teinte HSL tirée par anneau ; saturation 70 % et luminosité 65 % sont fixes dans la chaîne hsla(). 0 + 40 × Math.random() = rouges et oranges ; une constante = anneaux unis. |
Alpha de la bordure (JS — , 0.6)) |
.6 | Opacité de départ de la couleur de bordure, multipliée ensuite par l'opacity animée. .9 rend l'anneau franc (≥ 3,5:1 sur le fond) ; .35 le fond dans le dégradé. |
Taille finale (CSS — @keyframes cursorRippleExpand, 100 %) |
150 px | Diamètre atteint à la fin. Modifiez width et height ensemble. Comme scale passe aussi de 0 à 1, l'anneau reste petit longtemps : à mi-parcours il ne mesure que 70 px sur 150. |
Durée (CSS — animation: cursorRippleExpand 1s) |
1s ease-out | Vie de l'anneau et vitesse d'ouverture. Le retrait du div suit automatiquement (animationend). .6s = gouttes nerveuses ; 2s = ondes lentes, jusqu'à 16 anneaux présents au verrou par défaut. |
Épaisseur (CSS — border: 2px solid) |
2 px | Trait de l'anneau. Notez que scale(0 → 1) agit sur tout l'élément : la bordure paraît plus fine à la naissance et n'atteint 2 px qu'à la fin. 3px pour un anneau qui reste visible dès le départ. |
Départ progressif (CSS — scale(0) à 0 %) |
scale(0) + width 0 | Remplacez scale(0) par scale(1) à 0 % pour n'animer que la largeur : l'anneau s'ouvre alors vite au début et ralentit, courbe ease-out classique, et la bordure garde son épaisseur du premier au dernier instant. |
FAQ
width/height de 0 à 150 px) et son scale (de 0 à 1). Le diamètre affiché est le produit des deux ; en début de courbe, deux petites valeurs multipliées donnent presque rien — mesuré, 1 px à 50 ms et 14 px à 200 ms, puis 70 px à 500 ms et 150 px à 1 s. C'est une propagation qui accélère, à l'inverse d'une goutte réelle. Pour une ouverture rapide qui ralentit, mettez scale(1) dès l'image 0 % et laissez la largeur seule porter l'animation.animationend et s'enlève quand l'animation d'une seconde se termine ; il n'y a pas de minuterie ni de boucle. Au débit maximal, 8 anneaux par seconde pendant 1 s = 8 div présents au plus, mesurés 7. La faiblesse de ce choix : si l'animation ne joue pas — animation: none posé par une règle de mouvement réduit, ou un navigateur sans animations CSS — l'événement ne vient jamais et les div s'accumulent, invisibles mais réels. Dans ce cas, ne créez pas d'anneau du tout (condition matchMedia autour de l'écouteur) plutôt que de cacher ceux qui existent.querySelectorAll('[data-ripple-field]') et donne à chacune son propre verrou et ses propres anneaux. Toute la page est possible en posant l'attribut sur un conteneur en position: relative et overflow: hidden qui l'englobe — les left/top des anneaux sont relatifs à ce conteneur, pas à la fenêtre : s'il défile, les anneaux nés en haut restent en haut. Pour des anneaux fixes à l'écran, il faudrait passer .alchemy-cursor-ripple en position: fixed et utiliser clientX/clientY sans soustraire le rectangle de la zone.