Water Ripple
Midnight-blue water, smooth and still, where every click drops a disturbance that spreads in circles, bounces off the edges and crosses the waves of other clicks: a real wave equation on a height grid, recomputed pixel by pixel — 4.7 KB of JavaScript, sharp on Retina screens, stopped when the water is calm.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. Liquid has 31 effects, including 6 free. Explore the category →
3 usage examples



How it works
The .fx-0392-root block is 300 px high and takes the full available width. Its .water-ripple-container (pointer cursor) holds a <canvas class="water-ripple-canvas"> set to display: block at 100% of the frame, and a help line at the bottom, "Click to create ripples". The height is set by the root's CSS and the canvas fits into it: 20 resizes in a row, 0 px of drift (Chromium and Firefox). Still water is a flat midnight blue, rgb(10, 45, 100).
The script keeps two height grids (Float32Array), the current state and the previous one, one cell per canvas pixel. A click drops a 5 px radius disc whose height is 500 at the center and decreases toward the edge. On every frame, each cell takes the average of its four neighbors in the previous state, times 2, minus its own value, then loses 1% (damping = 0.99): that is the discrete form of the wave equation. The disturbance spreads in rings, reflects off the edges (edge cells stay at 0) and rings from several clicks cross without getting in each other's way. The rendering reads each height: a crest lightens the blue (up to 255), a trough darkens it, and red rises with the amplitude, hence the purple rim at the point of impact.
The loop only runs during a wave. At rest, no frame is computed (0 calls to requestAnimationFrame per second, measured); after a click, it stops by itself when the highest point drops back under 0.5, about 900 frames (15 s at 60 Hz), then resets the grid to zero. It also pauses off screen (IntersectionObserver) and when the tab is hidden. On a Retina screen, the grid holds 2 cells per CSS pixel as long as it stays under 800,000 cells (a 640 × 300 block gives 1,280 × 600, sharp); the script then runs 2 computing steps per frame, with the 0.99 damping split across both, so the wave travels and fades at the same pace to the eye. Measured during a wave: 1.1 ms per frame in Chromium, 1 ms in Firefox and WebKit at 640 × 300; 6, 9 and 5 ms on Retina. The script animates every .fx-0392-root on the page separately.
Accessibility
- Reduced motion: under
prefers-reduced-motion: reduce, a click does not start a wave. A light spot with a 16 px radius appears under the pointer and fades in place within about forty frames (0.7 s at 60 Hz), with no spreading ring; measured in Chromium and Firefox, the loop then stops by itself. At rest, the water is still in both modes. - Keyboard: the water reacts to a click (mouse or finger), not to the keyboard. If the gesture triggers an action (picking a zone, counting), also give it a button or link reachable by keyboard, and announce the result in a
role="status"area, like the counter and the card in the examples. - Content: the canvas tells screen readers nothing. The help text "Click to create ripples" is real text; in an actual interface, replace it with an instruction that says what the gesture is for.
- Contrast: the help text, white at 50% on midnight blue, reaches 4.45:1 (computed), just under the 4.5:1 threshold for small text: raise it to 60% opacity if it carries a useful instruction. Text placed over the water stays readable while waves pass underneath: they only shift the background by a few shades of blue.
Browser compatibility
Canvas 2D (createImageData, putImageData), Float32Array, ResizeObserver, IntersectionObserver, matchMedia: 4.7 KB of JavaScript, no dependency. Measured in Chromium 151, Firefox 153 and WebKit 26: same wave, same reflection off the edges, canvas at 1,280 × 600 on a Retina screen, loop stopped at rest, still spot under reduced motion, 0 errors. Computing steps are counted per displayed frame: on a 120 Hz screen the wave spreads twice as fast as at 60 Hz.
Without ResizeObserver, the script follows the window resize event; without IntersectionObserver, a wave keeps computing off screen until it fades out. Without JavaScript, the frame stays empty with its help text: add a fallback background on .water-ripple-container if the water must show.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0392-root">
<div class="water-ripple-container">
<canvas class="water-ripple-canvas"></canvas>
<span class="water-hint">Click to create ripples</span>
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Damping (JS — damping, computed from 0.99) |
0.99: the wave loses 1% per frame | 0.98 fades the wave out in half the time, with shorter circles; 0.995 lets it bounce from edge to edge for a long time. Stay under 1, or the water never calms down. |
Drop size and strength (JS — (still ? 16 : 5) * scale and 500) |
5 px radius disc (16 px under reduced motion), height 500 | A radius of 10 instead of 5 gives wider, softer rings; a height of 250, subtler circles. Blue caps out as soon as a crest goes above 155: beyond that, only red rises and the impact turns purple. |
Water colors (JS — render function) |
rest rgb(10, 45, 100); crest toward bright blue | Blue is 100 + value, green (150 + value / 2) × 0.3, red 10 + |value| × 0.2. For turquoise water, raise the green factor to 0.8; for dark water, lower the blue's 100 to 60. |
Retina sharpness (JS — var PIXEL_BUDGET = 800000) |
800,000 cells at most | Beyond that (a block larger than 200,000 CSS pixels, about 640 × 310), the grid switches to one cell per CSS pixel, slightly less sharp but with 4 times fewer cells to compute. Raise the value for a large, sharp block on desktop. |
Height (CSS — height of .fx-0392-root) |
300px | The canvas follows the block's height. For full-screen water, set the root to position: absolute; inset: 0. |
Help text (HTML — .water-hint) |
"Click to create ripples" | Replace it with your interface's instruction, or hide it (display: none) if the page already explains the gesture. |
FAQ
click listener. Call the same code on pointermove with a low height (50 instead of 500), at most once every 50 ms, for a wake that follows the pointer. The loop restarts by itself with each drop and stops when the water is calm again.