# Water Pebbles

> 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.

Canonical: https://gsapvault.com/effects/water-pebbles
Live demo: https://gsapvault.com/demos/water-pebbles/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £5 |
| Difficulty | advanced |
| Plugins | Core GSAP only |
| Techniques | webgl-shader, wave-simulation, refraction, draggable |
| Uses Lenis | No |

## Overview

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.

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.

## Features

- 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

## 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

## Vibe-Code Ready Setup

This effect includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

### 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.

## Documentation

How this effect works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### 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.

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium effect. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required for our code. Bundled third-party assets retain their own licences and attribution requirements. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Water Pebbles](https://gsapvault.com/effects/water-pebbles)
- [The Vault (£99 one-time, best value): every collection in the Vault library, plus future items added to those collections](https://gsapvault.com/effects)

## Judge the Code Quality First

These related effects are free with complete source published, written to the same production standard (cleanup functions, reduced-motion support, framework-agnostic):

- [Parallax Hero](https://gsapvault.com/effects/parallax-hero.md): A pinned hero that pulls its layers into depth from one scrubbed ScrollTrigger: a headline sandwiched inside the stack, ridges or photographs climbing over it, and a sun that sets behind the far layer.

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
