Quick Start
1. Add to your HTML <head>:
Code snippet omitted: it ships with the download.
2. Add before the closing </body> tag:
Code snippet omitted: it ships with the download.
3. Add the effect HTML anywhere in your <body>:
The wrapper needs data-liquid-ripple. Inside it, a .ripple-frame holds a .ripple-svg whose scene group carries the filter, plus the filter definition itself. The class names below are what the script queries, so keep them.
Code snippet omitted: it ships with the download.
That is all you need. An optional .ripple-splash-btn button anywhere on the page (the demo puts one in its toolbar) gives keyboard users a way to trigger the splash.
Using It With Your Own Design
What the script needs from your markup. A wrapper carrying data-liquid-ripple, and inside it the .ripple-frame, .ripple-svg, .ripple-scene (with data-filter-id naming the filter), .ripple-turb and .ripple-disp nodes exactly as in Quick Start. Those class names are the effect's API: the script writes scale and baseFrequency onto the two filter primitives every frame and attaches the filter to the scene group by id. .ripple-glow and .ripple-splash-btn are optional and are simply skipped when absent. Give every instance on the page its own filter id.
Rippling only part of the picture. The demo displaces the sea but not the beach. That is two copies of the same <image>: a sharp one drawn first (.ripple-base, outside the scene group, never filtered) and the filtered copy inside .ripple-scene with mask="url(#lr-water)". The mask is a white path covering the water, drawn in viewBox units and softened with a small feGaussianBlur so the boundary reads as wet sand rather than a cut. Trace the path for your own photograph (a few cubic segments along the edge is enough), or delete the base image and the mask attribute to ripple the whole frame. The script never touches either, so the mask is purely markup.
What is only the demo. The dark grey ground, the toolbar prompt, the hairline frame with its 8px radius, and the .ripple-overlay headline and caption sitting on the dark water are the demo's composition. Delete or restyle all of it; the ripple does not depend on any of it. The demo's assets/img/shore.webp is a placeholder photograph, replace it with yours.
CSS the effect relies on. .ripple-frame must be position: relative with overflow: hidden: the displacement pushes pixels past the image edge and the frame clips them, and the glow is positioned inside it. .ripple-svg needs transform-origin: 50% 50% because the script scales it up to 3% as the swell. The frame's touch-action: pan-y is what keeps a phone scrolling past the image (see Mobile below); do not set it to none unless the image is a self-contained element the page can scroll around. There is no pre-hide gate: the filter is attached by the script, so without JavaScript the image is simply sharp and no CSS restore is needed.
Using your own image
Two important details:
- Oversize the artwork. At full splash the displacement pushes the pixels outward; if the image only just fills the frame you will see gaps at the edges. Place it larger than the
viewBox(the demo draws its image fromx="-100"throughwidth="1000"on an800-wide box) so the edges always have material to ripple. - Same-origin images only for
<image href>. A cross-origin image inside an SVG filter can taint the canvas and silently drop the filter in some browsers. Host the image yourself (the demo ships a localassets/img/shore.webp) rather than hotlinking a remote URL.
Swap assets/img/shore.webp for your own photo, or replace the <image> with an SVG drawing (shapes, gradients). Either way, high-contrast horizontal edges (a horizon, wave foam) ripple most vividly.
Options
Set these as data attributes on the data-liquid-ripple wrapper.
| Attribute | Values | Default | Description |
|---|---|---|---|
data-ripple-strength |
Any number (px) | 62 |
Peak displacement a hard flick can reach. Above ~90 the edges tear; below ~30 the splash reads timid. |
data-ripple-frequency |
0.004–0.02 |
0.009 |
Resting turbulence frequency. Higher makes finer, busier ripples. |
data-ripple-gain |
Any number | 0.85 |
How strongly cursor speed maps to displacement. Raise for a twitchier surface. |
data-ripple-idle |
true, false |
false |
Adds a gentle idle micro-ripple. Off by default so rest reads crisp. |
data-ripple-drop |
Seconds, 0 to disable |
0 |
Drops a stone at a random point every N seconds while the pointer is away from the image, so the surface reads without interaction. Stops for good once the visitor disturbs the surface themselves. The demo uses 4. |
Accessibility
This effect is built to degrade cleanly:
- No JavaScript: the filter is never attached, so the image shows crisp and undistorted. The distortion is purely an enhancement.
- Reduced motion: under
prefers-reduced-motion: reducethe script skips all wiring; the image stays static and sharp and the glow is hidden. - Keyboard: the optional
.ripple-splash-btntriggers the same splash a tap does, so the signature moment is reachable without a pointer. Give it a clear label. - Touch: pointer events cover touch, so a sideways finger drag ripples and a tap splashes. The frame keeps
touch-action: pan-y, so a vertical swipe scrolls the page as normal.
Mobile / Touch
On coarse pointers a sideways drag produces the same ripple, and a tap drops a one-shot splash that settles on its own. The frame keeps touch-action: pan-y, so a vertical swipe over the image scrolls the page rather than rippling it: the image never traps a visitor who is trying to scroll past. If the image is a self-contained element with room to scroll around it and you want vertical drags to ripple too, set touch-action: none on .ripple-frame in your own CSS.
Dependencies
Required:
- GSAP 3.12+ (core only, no plugins)
Optional:
- None
Credits
Demo photograph (aerial surf on a dark shore) from Pexels, used under the Pexels License and baked to a local assets/img/shore.webp (no hotlinking). Replace it with your own image before shipping.
Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.