Cursors✨ Premium

Cursor Ripple Field

At most one ring every 120 ms: a div with a 2 px border, hue drawn between 220° and 280°, growing from 0 to 150 px in 1 s — width and scale animated together, hence a slow start — while its opacity fades out, then removing itself on animationend.

JSAnimationRipple

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

Breathe — hero of a meditation or sleep app — Cursor Ripple Field example 1

① Breathe — hero of a meditation or sleep app

WhenDark home screen of a wellness app: title, session length, one button, all on a night gradient.
WhyCircles that open slowly and fade evoke a drop on water and a breathing rhythm; the code's blue-violet hues match the night theme with no tuning. The 8-per-second cap stays calm even under a restless mouse.
SettingsLock raised to 200 ms (5 rings/s), duration 1.6s for a slower spread, final size 220px, border alpha .45 to stay behind the title.
Sonar — network monitoring panel — Cursor Ripple Field example 2

② Sonar — network monitoring panel

WhenDashboard card showing nodes (servers, sites) on a gridded background, with a status header.
WhyThe ring spreading from the pointer borrows the visual code of sonar and ping: it gives the panel a “the network responds” reading without a permanent animation that would distract from the numbers.
SettingsHue fixed to the status green (hsla(160, 70%, 60%, .7)), duration .8s, final size 120px, lock 150 ms; cursor: default, since the panel isn't clickable.
Ambient player — interactive album artwork — Cursor Ripple Field example 3

③ Ambient player — interactive album artwork

WhenFull-frame visual of an audio player (title, artist) with the transport bar at the bottom.
WhyOn abstract artwork, the rings extend the music in response to the gesture; the overflow: hidden clipping keeps them inside the visual and away from the controls. The blue → violet palette is that of ambient and electronic music.
SettingsHue range widened to 200 + 100 × Math.random() (cyan → magenta), 3px border, final size 260px, duration 1.4s.

How it works

Every [data-ripple-field] gets a mousemove listener guarded by a time lock: a boolean t flips to true on the first emission and a 120 ms setTimeout releases it. Any movement during that delay is ignored, whatever the mouse polling rate: output is capped at 1000 / 120 ≈ 8 rings per second. Measured in Chromium: 9 rings created over 1.29 s of continuous motion, at most 7 present at the same instant.

For each accepted emission the script creates a div.alchemy-cursor-ripple, positions it with left/top at the pointer coordinates relative to the zone (getBoundingClientRect) and gives it a border color of hsla(220 + 60 × Math.random(), 70%, 65%, .6) — a hue between blue (220°) and violet (280°), at 60% opacity. The div is appended to the zone and self-destructs: an animationend listener calls remove(). There is no requestAnimationFrame loop and no cleanup timer: the end of the CSS animation is the only clock.

The motion is entirely CSS: .alchemy-cursor-ripple is position: absolute, border: 2px solid, round, pointer-events: none, and plays cursorRippleExpand 1s ease-out forwards. The keyframes animate two quantities at once: width/height from 0 to 150 px and transform: translate(−50%, −50%) scale(0 → 1). The visible diameter is their product: measured, 1 px at 50 ms, 14 px at 200 ms, 70 px at 500 ms, 150 px at 1 s — the ring starts very slowly then opens fast, whereas a single quantity in ease-out would do the opposite. Opacity goes from 1 to 0 on the same curve (0.69 at 200 ms, 0.32 at 500 ms). The translate(−50%, −50%) centers the ring on the emission point.

The .ripple-field-zone is 100% × 300px, with a linear-gradient(135deg, #0f0f2e, #1a1a3e), cursor: pointer, position: relative (anchor for left/top) and overflow: hidden, which clips rings born near the edges. The sold CSS is limited to that zone, .alchemy-cursor-ripple and its cursorRippleExpand keyframes: nothing from another effect.

Accessibility

  • prefers-reduced-motion not handled: rings open regardless of the system preference. Add @media (prefers-reduced-motion: reduce) { .alchemy-cursor-ripple { animation: none; display: none } } — careful, without an animation the animationend event never fires and the divs would no longer be removed, hence the display: none; better still, gate the listener on matchMedia('(prefers-reduced-motion: reduce)').matches so nothing is created.
  • The effect is mouse-only: no ring from the keyboard, and on touch at most one per tap (a single emulated mousemove). The zone is a non-focusable div; cursor: pointer promises a click that doesn't exist in the code — replace it with default if the zone isn't clickable, or wire a real action.
  • Contrast: the rings are decorative graphical elements. At 60% opacity on the gradient, the least visible hue measures 2.3:1 and the most visible 2.9:1, below the 3:1 threshold for UI components (WCAG 1.4.11) — fine for an ornament, not if the ring must signal something: raise the alpha to .9 (≥ 3.5:1). The shipped hint text, rgba(255,255,255,.15), sits at 1.5:1: raise it to .5 (5.3:1) if it must be read.
  • Screen readers: the divs are empty, hence silent, but up to 8 insertions and removals per second churn the accessibility tree. Put aria-hidden="true" on the [data-ripple-field] zone. The native pointer stays displayed (no cursor: none): nothing is taken away from the user.
  • Integration: the zone must keep position: relative (otherwise the rings position themselves against the nearest positioned ancestor) and an explicit height — the sold code sets 300px; a % value inside a parent without a height renders 0 px. Keep overflow: hidden, which clips the 150 px rings born at the edge. No reset on exit: each ring removes itself after one second.

Browser compatibility

ES5 code (var, function): it requires getBoundingClientRect, setTimeout, the animationend event, a CSS animation on width/height, transform and opacity, and hsla() colors. Zero dependencies.

Chrome 43+✓ Full
Firefox 16+✓ Full
Safari 9+✓ Full
Edge 12+✓ Full
Mobile iOS✓ Static (no mousemove)
Android Chrome✓ Static (no mousemove)

Without JavaScript, the zone keeps its gradient and hint text: no ring, nothing broken. Without CSS animations (very old browsers), a ring would be created at 0 px, invisible, and never removed for lack of animationend — one empty div per emission, with no visible consequence.

The code

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

index.html — structure
<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;">Move the cursor to create ripples</div>
</div>
🔒 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
Lock (JS — setTimeout(..., 120)) 120 ms Minimum interval between two rings, hence the maximum rate (1000 / 120 ≈ 8 per second). 60 doubles the density; 250 gives isolated rings, readable one by one. The number of simultaneous rings equals duration / lock.
Hue (JS — 60 × Math.random() + 220) 220° to 280° (blue → violet) HSL hue range drawn per ring; saturation 70% and lightness 65% are fixed in the hsla() string. 0 + 40 × Math.random() = reds and oranges; a constant = uniform rings.
Border alpha (JS — , 0.6)) .6 Starting opacity of the border color, then multiplied by the animated opacity. .9 makes the ring crisp (≥ 3.5:1 on the background); .35 melts it into the gradient.
Final size (CSS — @keyframes cursorRippleExpand, 100%) 150 px Diameter reached at the end. Change width and height together. Since scale also goes from 0 to 1, the ring stays small for a long time: halfway through it measures only 70 px out of 150.
Duration (CSS — animation: cursorRippleExpand 1s) 1s ease-out Ring lifetime and opening speed. Removal of the div follows automatically (animationend). .6s = nervous drops; 2s = slow waves, up to 16 rings present at the default lock.
Thickness (CSS — border: 2px solid) 2 px Stroke of the ring. Note that scale(0 → 1) acts on the whole element, so the border looks thinner at birth and reaches 2 px only at the end. 3px for a ring that stays visible from the start.
Progressive start (CSS — scale(0) at 0%) scale(0) + width 0 Replace scale(0) with scale(1) at 0% to animate the width only: the ring then opens fast at first and slows down, the classic ease-out curve, and the border keeps its thickness from the first instant to the last.

FAQ

Because two quantities grow at the same time: the box (width/height from 0 to 150 px) and its scale (from 0 to 1). The displayed diameter is the product of both; early on the curve, two small values multiplied give almost nothing — measured, 1 px at 50 ms and 14 px at 200 ms, then 70 px at 500 ms and 150 px at 1 s. It is a propagation that accelerates, unlike a real drop. For a fast opening that slows down, set scale(1) from the 0% frame and let the width alone carry the animation.
Each ring listens to its own animationend and removes itself when the one-second animation ends; there is no timer and no loop. At the maximum rate, 8 rings per second for 1 s = at most 8 divs present, 7 measured. The weakness of this choice: if the animation doesn't play — animation: none set by a reduced-motion rule, or a browser without CSS animations — the event never fires and the divs pile up, invisible but real. In that case, create no ring at all (a matchMedia condition around the listener) rather than hiding the ones that exist.
Several zones, yes, with no change: the script runs querySelectorAll('[data-ripple-field]') and gives each one its own lock and its own rings. The whole page is possible by putting the attribute on a position: relative, overflow: hidden container that wraps it — the rings' left/top are relative to that container, not to the viewport: if it scrolls, rings born at the top stay at the top. For rings fixed on screen you would switch .alchemy-cursor-ripple to position: fixed and use clientX/clientY without subtracting the zone's rectangle.