Liquid✨ Premium

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.

CanvasInteractivePhysics

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

Bashō's old pond — haiku of the day in a Japanese poetry club app — Water Ripple example 1

① Bashō's old pond — haiku of the day in a Japanese poetry club app

WhenOn a full-frame midnight-blue background, the haiku of the day shows in three lines at the bottom left: "The old pond — a frog jumps in, the sound of water". Touching the water starts circles where the finger lands.
WhyThe wave starts exactly at the touched point and spreads in circles over still water: the poem describes that very gesture. A spray of splashes would be too loud for a haiku.
SettingsFull-frame block (position: absolute; inset: 0), help text hidden; dark overlay at the top and bottom for the title and the poem, text laid on top with pointer-events: none so the click reaches the water; poem in 21 px serif, indented by 22 px on each line.
Swimming zones on a lake — touch map at a leisure park's lifeguard station — Water Ripple example 2

② Swimming zones on a lake — touch map at a leisure park's lifeguard station

WhenThe top-down lake map shows three marked zones, "Shallow end", "Deep end" and "Paddle". The visitor touches a zone: the wave starts from their finger, the zone name turns yellow and shows in the card with the water temperature, 21 °C.
WhyThe wave confirms where the finger landed and the rest of the lake stays calm: the chosen zone is spotted at a glance, with no added cursor or highlight.
SettingsFull-frame water; beach and lawn drawn in SVG on top, dotted swim lines, all with pointer-events: none; a scene script listens to the same click as the effect on .water-ripple-container and checks which zone it falls in; info card with role="status".
Making rings in the water — fine motor skills exercise in a rehab center app — Water Ripple example 3

③ Making rings in the water — fine motor skills exercise in a rehab center app

WhenOn the tablet in front of the patient, the instruction "Touch the water with your fingertip, once per ring" and a "12 rings" counter at the top right; each touch makes a ring and adds 1.
WhyThe circle starts right under the finger, to the pixel, then fades slowly: the patient sees how precise the gesture was and the rhythm they keep, which a brief splash does not show.
SettingsWater under a light 58 px header bar (root at top: 58px); counter with role="status", incremented by a click listener on .water-ripple-container; help text replaced by the instruction.

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.

Chrome 64+✓ Full
Firefox 69+✓ Full
Safari 13.1+✓ Full
Edge 79+✓ Full
Mobile iOS 13.4+✓ Full (by touch)
Android Chrome 64+✓ Full (by touch)

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

index.html — structure
<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 / propertyDefaultEffect
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

The grid's edge cells are never recomputed and stay at 0: for the wave equation, that is a wall. A wave that reaches it heads back the other way, as in a pool. For water that escapes through the edges, you would need an absorbing layer (stronger damping on the last cells), which this code does not do.
Water Ripple simulates a surface: a slow wave that spreads, bounces and mixes with others for several seconds, on water that stays smooth at rest. Splash Effect throws a spray of 30 droplets and three rings that fall back in under a second, on a dark background. Pick Water Ripple to show where the finger landed and leave the trace for a while; Splash Effect for a brief, energetic reaction.
Yes: the code that drops the disturbance is in the container's 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.