090

Water Pebbles

£5

A hero background of clear, shallow water over a pale stone floor, with smooth river pebbles on the bottom seen through the ripples. Click the water to drop a pebble in with a splash, drag one along the floor to push a bow wave and nudge the stones it meets.

advanced
4 more details
webgl-shaderwave-simulationrefractiondraggable
Water Pebbles - GSAP animation effect preview

This demo reads better at your own screen size than in the frame below:

About this effect

A hero background that turns the top of the page into a shallow tray of clear water over a pale stone floor. Smooth river pebbles lie on the bottom, seen through a gently moving surface: they bend with the ripples, catch drifting caustic light and carry soft shadows on the floor beneath them.

Read the full effect overview

Click or tap the water and a new pebble drops in with a splash, sinks out of focus and settles. Press a pebble and drag it along the floor: it trails your pointer with some weight, pushes a bow wave ahead of it and knocks the stones it runs into, which slide and turn before coming to rest. Arrow keys push the last pebble you touched, and your own buttons can drop pebbles or clear the tray.

It is three.js and a single full-screen shader, with no images to host: the floor takes its colour from your page, every stone is generated with its own shape, tone and markings, and pebbles keep clear of any headline you mark. Without JavaScript, without WebGL or under reduced motion it shows a composed CSS still of pebbles resting on the floor instead.

What's included

13 items
  • Real refraction: the floor and every pebble are seen through the moving surface, so ripples visibly bend the stones beneath them
  • Click or tap to drop a pebble that splashes, sinks out of focus and settles onto the floor
  • Drag a pebble with weight: it trails the pointer, pushes a bow wave and nudges the stones it meets
  • Stones collide and turn on a glancing knock, heavier pebbles shoving lighter ones further
  • Every pebble is generated, not a photo: its own outline, grey tone, mottling, speckle and sometimes a pale quartz vein
  • The floor colour comes from your page's --page custom property, so the canvas always matches the surrounding design
  • Keep-clear zones: pebbles stay out from under any element you mark, so headlines on top stay readable
  • Starting layouts set in markup, with a separate layout for narrow screens
  • Drop and clear from your own buttons through two simple events
  • Keyboard control: arrow keys push the last pebble you touched
  • On touch, vertical swipes over open water still scroll the page
  • Renders only while on screen, and disposes the WebGL context and every GPU resource on teardown
  • A composed CSS still for no JavaScript, no WebGL and reduced motion

Perfect for

5 use cases
  • Spa, wellness and skincare landing pages that want a calm, tactile first impression
  • Architecture, interior and landscape studios working with natural materials
  • Product launches for stone, ceramic, bath or homeware brands
  • Meditation and mindfulness apps looking for a hero people want to play with
  • Portfolio and agency sites that need one memorable, interactive above-the-fold moment

How it works

5 sections

One shader, no images

The whole scene is a single full-screen three.js shader. The floor, the stones, their shadows, the caustic light and the reflections on the surface are all drawn per pixel, so there are no textures to host and the scene stays sharp at any size and pixel density. The floor takes its colour from your page, which is why the hero blends into the design around it.

Water you can disturb

A small ripple simulation runs alongside the renderer. Drips, splashes, dragged stones and released stones all disturb it, and the rings spread, bounce and die away naturally. Everything beneath is seen through that surface, so a passing ripple visibly bends the pebbles and brightens and darkens the floor like light through real water.

Pebbles with weight

Each pebble is a physical body on the floor. A held stone trails your pointer rather than snapping to it, pushes a wave ahead of itself as it moves and shoves the stones it meets, lighter ones further than heavy ones, turning them slightly on a glancing hit. Released stones slide to a stop. Stones also slide out from under any content you mark to keep clear.

Played moments on one clock

The short, authored moments are GSAP tweens: a dropped pebble fading in and sinking into focus, a stone lifting slightly while held, and pebbles fading away when the tray is cleared or the oldest is replaced. The continuous simulation runs on the same GSAP ticker, so tweens and physics never drift apart, and the loop pauses whenever the effect is off screen.

Graceful fallback

A tiny check before first paint decides whether a canvas is coming. If it is not, because there is no JavaScript, no WebGL or the visitor prefers reduced motion, the composed CSS still of pebbles is what shows, and it is never hidden until a canvas actually exists. Tearing the effect down puts that still back and releases the WebGL context.

Difficulty Advanced
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Paid effect

Purchase to unlock the code.

Buying Water Pebbles opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects.

£5 Standard license, unlimited projects

Browse free effects

Documentation

Quick Start

1. Add to your HTML <head>:

Code snippet omitted: it ships with the download.

The inline probe is not optional. The CSS still of pebbles is the fallback for no JavaScript, no WebGL and reduced motion, and without the probe it paints for the half second it takes three.js to arrive, then gets swapped for the canvas. The probe runs before first paint, stamps html.gl when a canvas is coming, and the stylesheet hides the still under that class. No JavaScript, no class, still shows; and the effect removes the class again if it cannot build a renderer after all.

2. Add to your <body>:

Code snippet omitted: it ships with the download.

3. Add before the closing </body> tag:

Code snippet omitted: it ships with the download.

The script finds every [data-pebbles] element, inserts a canvas as its first child, and hides the still once the canvas is actually there.

Why the import map rather than a plain <script src> for three.js: three ships as ES modules only, and its old UMD build logs a deprecation warning on every page load. The shim above imports it as a module, puts it on window, and then loads assets/script.js as an ordinary script, so the effect itself stays a plain file you can drop into any build (or none).

Already using three.js as a module? Then skip the shim, and just make sure window.THREE is set before assets/script.js runs:

Code snippet omitted: it ships with the download.


Using It With Your Own Design

What the effect actually requires of your markup: one element with data-pebbles. That is all the script reads. Anything inside it (headline, buttons, links) sits on top of the water; the canvas goes underneath as the first child.

The water floor is your page colour. The shader reads the --page custom property from the root when it starts and uses it as the floor of the tray, so the canvas and the surrounding page always match. Set --page on .pebbles or :root to retint it; any CSS colour syntax works. Pale, neutral grounds read best: the stones are drawn in a natural grey palette and the caustics and reflections are light, so a dark floor loses both.

Non-obvious things the effect depends on (keep these if you restyle):

  • The container must have a real size. The canvas and the ripple grid are sized from the container, so .pebbles needs a height from somewhere: it is min-height: 100svh in the demo. Give it a height of 0 and you get a canvas of 0.
  • position: relative, isolation: isolate and overflow: hidden on .pebbles, and z-index: -1 on .pebbles__canvas. Together they keep the canvas behind your content without escaping the hero.
  • touch-action: pan-y on .pebbles. Vertical swipes on open water still scroll the page; only a touch that lands on a pebble holds the page still so the pebble can be dragged.
  • The .pebbles__still fallback must stay in the document. It is hidden by html.gl before first paint and removed by .pebbles.is-live, a class the script adds only once a canvas exists.
  • Mark your copy with data-pebbles-avoid. Pebbles are kept clear of those elements (stones pushed into the zone slide back out, and a held stone stops at its edge), and a click inside the zone makes a ripple instead of dropping a pebble. Without it, stones can come to rest under your headline. The zones are re-read on resize and once web fonts have loaded.
  • Buttons, links, inputs and anything with role="button" inside the root keep their normal clicks; the water ignores presses that start on them.

What is only the demo's CSS and can be deleted: .demo-title, .demo-controls, .demo-controls__group, .demo-controls__btn, the .demo-cue* rules and the Mona Sans import. The palette tokens other than --page are demo chrome too. The CSS still's positions, sizes and tones are yours to change: keep it looking like the live scene.


Options

All set on the [data-pebbles] element. Sizes are CSS pixels; below 640px container width, pebble sizes are scaled to 80%.

Attribute Values Default Description
data-spots x,y[,size]; ... none The pebbles already lying on the floor when the effect starts. x and y are fractions of the container (0 to 1), size is the stone's approximate radius in px. Omit size for a random size between data-min-size and data-max-size. Without this attribute the tray starts empty.
data-spots-narrow same format uses data-spots Starting layout used instead when the container is narrower than 640px at start-up. Portrait screens usually want fewer, larger-spaced stones.
data-max 1 to 14 14 Most pebbles on the floor at once. Dropping one past the limit fades the oldest out. Also caps how many data-spots are placed.
data-min-size number 14 Smallest random pebble size, in px.
data-max-size number 40 Largest random pebble size, in px. Dropped pebbles land between the two.
data-ambient 1, 0 1 Occasional ambient drips on the surface and slowly drifting caustic light. 0 turns both off for a completely still tray until someone touches it (useful for screenshots and captures).
data-avoid CSS selector [data-pebbles-avoid] Elements the pebbles keep clear of, as described above.

Every pebble's shape, tone, markings and rotation come from a seed, so no two stones look the same, and the starting pebbles look identical on every load.

Example: a quiet mid-page band with a few large stones

Code snippet omitted: it ships with the download.

Example: keep pebbles clear of your own elements

Code snippet omitted: it ships with the download.


Accessibility

  • Keyboard: the root is focusable (tabindex="0"). Arrow keys give the last pebble you touched or dropped a push in that direction, and it slides and knocks whatever it meets; hold Shift for a bigger push. With nothing touched yet, the newest pebble moves. Dropping a pebble from the keyboard goes through a data-pebbles-add button (see Programmatic Control).
  • Touch: tap open water to drop a pebble; the drop happens on release and only if the finger has not travelled, so a swipe that scrolls the page drops nothing. Press on a pebble to drag it.
  • Screen readers: the canvas and the still are aria-hidden. Describe the scene and the keyboard controls in the root's aria-label, as the demo does.
  • Reduced motion: the WebGL scene never starts. The CSS still stays exactly as it is: a composed arrangement of pebbles resting on the floor. Switching the preference on while the page is open tears the live scene down and brings the still back.
  • Without JavaScript, or without WebGL: the same still, for the same reason. One fallback, three failure modes, and it is never hidden until a canvas has actually been created.

Dependencies

Dependency Version Required
three.js 0.180.0 Yes, as an ES module (see Quick Start)
GSAP core 3.15.0 (works on 3.12+) Yes: matchMedia branching, teardown, the frame clock, and the played moments (a pebble sinking in, lifting while held, fading out when removed)

No GSAP plugins. The ripples and the pebble physics are simulated every frame on GSAP's ticker, so the continuous water and the short tweens it reads share one clock; GSAP does not animate the water itself.

Everything the effect uses (WebGLRenderer, ShaderMaterial, PlaneGeometry, DataTexture) is long-stable three.js API, so pinning to a different version is a one-line change in the import map.


Browser Support

Anything with WebGL, which is every current browser. Support is probed before three.js is asked for a renderer, deliberately: three logs its own failure to the console, as errors, several times over, before it throws, so catching the exception would hide nothing and a visitor with WebGL disabled would get a console full of red on a page that had quietly fallen back. Probed first, that visitor simply gets the CSS still and a clean console.

Import maps are supported everywhere current. In a browser old enough to lack them the module never runs, window.THREE is never set, and the still is again what shows.

If the browser drops the WebGL context (a GPU reset, too many tabs), the loop pauses and resumes when the context is restored.

Performance

One draw call. The cost is the fragment shader, which runs for every pixel of the container: the floor, up to sixteen stone slots, the caustic net and the surface reflections. Stones only cost anything on the pixels near them, and the ripple simulation is a coarse grid on the CPU. The loop runs only while the effect is on screen, and pixel ratio is capped at 2 with a mouse and 1.5 on touch devices.

To buy back frames on low-end hardware: the container's size is the first lever, because every pixel of it is shaded, so a hero that does not need to be full viewport should not be. After that, lower data-max, since each stone adds shading work across its own patch of the screen.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.

Your cart

Your cart is empty

The Vault £99

The Vault library, plus future additions to the library.