Cursors✨ Premium

Cursor Ripple

A 100 ms throttle on mousemove sets the pace of ripple creation: each allowed pulse births an element at the pointer's coordinates, animates it through a CSS class, and removes it automatically 600 ms later — up to six rings coexist in the DOM at sustained speed.

JSRipple

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Cursors has 26 effects, including 1 free. Explore the category →

3 usage examples

Annotation zone on a collaborative whiteboard — Cursor Ripple example 1

① Annotation zone on a collaborative whiteboard

WhenA shared surface — a map, diagram, or canvas — where several users navigate and displaying other participants' cursors would overload the interface.
WhyRipples surface local activity without cluttering the view: each ring is born, expands, and vanishes in 600 ms. The 100 ms rhythm ties rings to the actual reading pace rather than an independent timer.
SettingsLifetime at 400 ms for snappier rings; throttle at 150 ms to limit accumulation during long sessions; elements inserted into the scrollable container using relative coordinates rather than appended to body.
Full-screen hero on a creative portfolio — Cursor Ripple example 2

② Full-screen hero on a creative portfolio

WhenA hero with no explicit CTA — textured background, typography alone — designed to be explored with the mouse before scrolling. The effect invites curiosity rather than immediate action.
WhyThe rings stay fixed at their birthpoint for the full animation: a fast pass leaves a staggered wake of fading circles — a result no cursor trail can replicate, since trail elements follow the pointer instead of marking where it was.
SettingsRing diameter enlarged to 60–80 px in the CSS class, offset adjusted accordingly; throttle lowered to 70 ms to densify the constellation; semi-transparent color so rings don't compete with the typography.
Active dropzone waiting for a file drop — Cursor Ripple example 3

③ Active dropzone waiting for a file drop

WhenA drag-and-drop zone in its active state — the user moves the mouse over the surface to confirm it is ready to receive the file.
WhyThe ripples confirm receptivity without extra text or a looping background animation. Rings follow the natural rhythm of the movement — more honest than a fixed CSS animation that loops identically regardless of the drag state.
SettingsThrottle at 180 ms for a calm pace; lifetime at 350 ms for sharper rings; color synchronized with the dropzone's state (green for accepted, red for rejected hover).

How it works

Before anything is created, the code passes through a time gate. On every mousemove event, it compares Date.now() against lastRippleTime: if less than 100 ms (rippleThrottle) have elapsed, the handler returns immediately. This delay is the effect's central control — it governs ring frequency far more than cursor speed does. At slow movement, ripples spawn apart and each one expands on its own. At fast movement, the gate allows up to ten pulses per second. When the cursor stops, no mousemove fires: the effect shuts itself off without any cleanup needed.

Each pulse that clears the gate assigns two classes to the new <div>: custom-cursor cursor-ripple. The first sets base properties for all custom cursor elements, including position: fixed; the second carries the @keyframes — the ring's outward expansion and fading opacity. The element is appended to document.body, not to the listening zone. This is consistent with the coordinates used: e.clientX and e.clientY are viewport values, and position: fixed interprets them in the same frame. A −10 px offset on each axis compensates for half the ring's implicit diameter so its origin centers on the cursor hotspot.

Once inserted, the element is autonomous. A setTimeout of 600 ms schedules its removal via ripple.remove() — no registry of in-flight elements, no interruption if the cursor leaves the zone. Lifetime (600 ms) divided by throttle (100 ms) gives a predictable maximum: up to six rings coexist simultaneously. At high speed they overlap in a wave trail; at low speed each ring completes before the next appears. Unlike a cursor trail, the rings stay fixed at their birthpoint — they propagate from where the cursor was, not where it is.

Accessibility

  • prefers-reduced-motion: absent from the code. The cursor-ripple class checks no system preference. Gate element creation on the JavaScript side with window.matchMedia('(prefers-reduced-motion: reduce)').matches before insertion, or wrap the @keyframes inside @media (prefers-reduced-motion: no-preference) on the CSS side.
  • On touch screens, mousemove does not fire in normal use: no ring is ever created, no cleanup is needed. For ripples on tap, a separate touchstart listener works — but the 100 ms throttle does not span across different touch points: a multi-touch gesture can produce simultaneous ripples.
  • Created elements land in <body> without aria-hidden. Depending on the screen reader's tracking mode, dynamically added nodes may be announced. Add ripple.setAttribute('aria-hidden', 'true') after the createElement call to explicitly exclude them from the accessibility tree — they are purely decorative.
  • With position: fixed, scrolling does not clear existing ripples: they stay anchored to the viewport until the timer fires. On a long page, rings created near the top may seem to float out of context mid-scroll. Reduce their lifetime or confine them to the parent zone using relative coordinates (see Customization).

Browser compatibility

Relies on arrow functions and const/let (ES2015), Date.now() (ES5), classList, and setTimeout. The real floor is set by arrow functions: IE 11 halts on a syntax error before the first mousemove. On the CSS side, position: fixed and @keyframes have been universal since 2012.

Chrome 45+✓ Full
Firefox 22+✓ Full
Safari 10+✓ Full
Edge 15+✓ Full
Mobile iOS✓ Inert (no cursor)
Android Chrome✓ Inert (no cursor)

Without JavaScript, no element is created and the zone stays inert — the fallback is built into the design. On IE 11, the syntax error on arrow functions stops the script before the first execution; the rest of the page's JavaScript is unaffected. If IE 11 is a hard target, transpile the script: the logic has no other dependency.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Creation throttle (rippleThrottle = 100) 100 ms Compared against the gap between two successive Date.now() calls. At 50 ms, the maximum number of simultaneous rings doubles. At 200 ms, it drops to three. Keep consistency with the lifetime: if the throttle exceeds 600 ms, rings will never overlap.
Ring lifetime (setTimeout 600) 600 ms The setTimeout duration and the @keyframes animation duration should stay aligned. If the animation lasts 400 ms but the timer fires at 600 ms, the element sits invisible in the DOM for 200 ms. Go below 300 ms for crisp, fleeting bursts; raise to 900 ms for rings that dissolve slowly.
Centering offset (−10 px on left and top) −10 px Compensates for half the ring's visual diameter. If you change .cursor-ripple's size — for example width: 40px — the offset must become −20. A wrong offset shifts the ring's center away from the cursor hotspot: the ripple spawns beside the pointer rather than beneath it.
Ring CSS class (cursor-ripple) cursor-ripple All visual properties — color, initial size, growth and fade keyframes — live in this class. Create variants (cursor-ripple--click, cursor-ripple--large) and switch the class based on context or state without touching the JavaScript.
Insertion target (document.body) document.body Appending to body with position: fixed anchors rings to the viewport. To confine them inside a scrollable container, insert them into the parent zone (position: relative) and compute relative coordinates: e.clientX - rect.left and e.clientY - rect.top. Also switch fixed to absolute in the CSS class.
Trigger event (mousemove) mousemove Replace with click for ripples on click only — the throttle becomes unnecessary, each click produces exactly one ring. Combine both by attaching two separate listeners: a mousemove for small movement rings, a click for a larger impact ring with a different class and a more pronounced animation.

FAQ

The answer is arithmetic: lifetime (600 ms) divided by throttle (100 ms) = 6 at most. This is expected behavior. On static content, six CSS-animated <div>s are negligible. If you notice an impact on a page already heavy with transforms, raise the throttle to 150–200 ms: the maximum drops to three or four elements without the eye noticing a difference.
Because e.clientX and e.clientY are viewport coordinates, and position: fixed interprets them in the same frame. Changing the insertion target without adapting the coordinates shifts the rings away from the cursor. To confine them within the zone, compute zone-relative coordinates and switch fixed to absolute in the CSS class.
No. With position: fixed, scrolling does not move them — they stay anchored to the viewport until the setTimeout fires. The code sets no mouseleave listener: a ring created near the edge completes its 600 ms even after the cursor has left. To cancel in-flight animations on zone exit, collect the ids returned by setTimeout in an array and call clearTimeout on each inside a mouseleave listener.