Three.js Scroll Shader Ripple Distortion — GLSL ShaderMaterial Driven by Scroll

Three.js Scroll Shader Ripple Distortion · Scroll · Plain HTML, CSS & JS · Live preview

What's included

Features

Full-viewport plane rendered with an OrthographicCamera for pixel-perfect coverage at any aspect ratio
Real, compiling GLSL ShaderMaterial — a passthrough vertex shader and a distortion fragment shader
Radial sine-wave UV distortion whose frequency and amplitude both scale with a single uProgress uniform
Three-color gradient mixed via chained smoothstep() calls, no image texture required
Ring highlight and vignette computed purely from distance-from-center math
GSAP ScrollTrigger scrub drives uProgress; a separate clock drives uTime for continuous motion
No per-frame CPU geometry work — all distortion happens on the GPU inside the fragment shader
Fully reversible — scrolling up smoothly relaxes the ripple back to its resting amplitude

About this UI Snippet

How to Build a Scroll-Driven GLSL Ripple Shader With Three.js

Screenshot of the Three.js Scroll Shader Ripple Distortion snippet rendered live

The Three.js Scroll Shader Ripple Distortion snippet renders a single full-viewport plane with a custom THREE.ShaderMaterial, whose fragment shader displaces its own UV coordinates with a sine-wave ripple radiating from center — and the ripple's frequency and amplitude both grow as the visitor scrolls through a pinned section, entirely via one uProgress uniform updated from JavaScript each frame.

An orthographic camera plus a 2×2 plane — the simplest full-viewport shader surface

Rather than sizing a PlaneGeometry to exactly match a PerspectiveCamera's frustum at some distance, this snippet uses an OrthographicCamera(-1, 1, 1, -1, 0, 1) paired with a plane spanning (-1, -1) to (1, 1) — the camera's view volume and the plane's extent are defined to match exactly, so the plane fills the canvas at any aspect ratio with zero perspective-projection math required.

The vertex shader is a pure passthrough

gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0) is the standard Three.js vertex transform, and vUv = uv hands the plane's built-in UV attribute to the fragment shader unchanged — all of the actual visual work happens in the fragment shader, which is where GLSL shader effects like this one typically live.

Radial sine-wave UV distortion

The fragment shader computes each pixel's distance from center (dist) and its direction (dir), then offsets the sampled UV coordinate along that direction by sin(dist * freq - uTime * speed) * amp — a textbook radial ripple. Both freq and amp are linear functions of uProgress, so scrolling further into the pinned section makes the ripple both faster (higher frequency) and more pronounced (higher amplitude).

A three-color gradient in place of a texture

Instead of sampling a loaded image texture, the shader mixes between three hardcoded colors based on the *distorted* UV's y-coordinate, using two smoothstep(...) calls chained together for a soft three-stop gradient — the ripple visibly warps the gradient bands themselves, since the color lookup uses distortedUv, not the original vUv.

Ring highlights and vignette, computed, not textured

A second use of the same sin() ripple value drives a thin glowing ring highlight via smoothstep(0.985, 1.0, abs(rings)), and a radial smoothstep vignette darkens the plane's edges — both are pure math on dist, adding polish with no additional texture lookups.

One uniform, updated once per frame

material.uniforms.uProgress.value = progress.t is set inside the requestAnimationFrame loop, reading a plain object kept in sync by a scrubbed GSAP tween — the render loop and the scroll-driven tween are decoupled, exactly like the particle system in Three.js Scroll Galaxy Formation, just applied to a shader uniform instead of a geometry attribute.

Customizing it

Swap the three hardcoded colors for your brand palette, change freq/amp's multipliers for a subtler or wilder ripple, or replace the fragment shader's gradient mix with a texture2D sample of a loaded image for a distorted-photo effect instead of an abstract gradient. Pair it with Three.js Scroll Shader Fluid Gradient Wave for a related uniform-driven GLSL technique.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You do not need to derive the radial distortion math or the uniform-driven scroll wiring from scratch. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to explain exactly how the sin(dist * freq - uTime * speed) term produces a radially expanding ripple, and why the color gradient is sampled using the distorted UV rather than the original one. The same assistant can help you extend it — ask it to swap the procedural gradient for a texture2D sample of a loaded image, add a second, slower ripple layer for a more complex interference pattern, or expose the ripple's base color and speed as easily tunable uniforms. Treat the code as a conversation starter, not a finished artifact.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "scroll-driven GLSL ripple distortion" in plain HTML, CSS, and JavaScript using Three.js with a custom ShaderMaterial, plus GSAP and GSAP's ScrollTrigger plugin, all loaded from a CDN (no bundler, no build step).

Requirements:
- A pinned section containing a full-size canvas, rendered with an OrthographicCamera set to view exactly [-1, 1] on both axes, and a single PlaneGeometry(2, 2) mesh using a custom THREE.ShaderMaterial, so the plane fills the viewport at any aspect ratio.
- A vertex shader that is a standard passthrough: pass the built-in uv attribute to a varying vUv, and set gl_Position using projectionMatrix * modelViewMatrix * vec4(position, 1.0).
- A fragment shader that computes each pixel's distance and direction from the UV center, then offsets the sampled UV coordinate along that direction using a sine wave of distance and time (uTime), producing a radial ripple distortion.
- Two uniforms driving the ripple: a uProgress float uniform (updated every frame from a value kept in sync with scroll position) that scales both the ripple's frequency and amplitude, and a uTime float uniform (updated every frame from an elapsed-time clock, independent of scroll) that drives the ripple's continuous phase.
- Render a smooth multi-color gradient (mixing at least two or three colors via smoothstep or mix()) sampled using the distorted UV coordinate rather than the original one, so the gradient bands themselves visibly ripple.
- Register a GSAP tween on a ScrollTrigger targeting the pinned section, with pin: true, start at top top, a numeric scrub, and a multi-hundred-percent end, animating a single plain progress value from 0 to 1 with linear easing, and write that value into the uProgress uniform every animation frame.
- Confirm scrolling back up smoothly relaxes the ripple's frequency and amplitude back toward their resting values.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<section class="rpd-stage" id="rpdStage">
  <div class="rpd-intro-overlay"><p>Scroll ↓ to intensify the ripple</p></div>
  <canvas id="rpdCanvas"></canvas>
  <div class="rpd-hud"><span id="rpdPct">0</span>% intensity</div>
</section>
<section class="rpd-bottom"><p>A single ShaderMaterial plane, distorted entirely on the GPU.</p></section>

Step by step

How to Use

  1. 1
    Load all three CDN scriptsAdd three.min.js, gsap.min.js, and ScrollTrigger.min.js from the CDN panel, in that order.
  2. 2
    Paste HTML, CSS, and JSA rippling full-viewport gradient plane renders inside a pinned stage with a live "% intensity" read-out.
  3. 3
    Scroll downThe ripple’s frequency and amplitude both grow as uProgress climbs from 0 to 1.
  4. 4
    Scroll back upThe ripple calms back to its subtle resting state exactly in reverse.
  5. 5
    Retune the rippleChange the freq and amp multipliers in the fragment shader for a gentler or wilder distortion.
  6. 6
    Swap in a textureReplace the gradient mix with a texture2D sample of a loaded image for a distorted-photo variant.

Real-world uses

Common Use Cases

Generative art and shader portfolios
Showcase real GLSL work with an effect that visibly demonstrates uniform-driven distortion.
Music and album launch pages
An abstract rippling gradient suits ambient, electronic, or experimental releases.
Agency or studio hero sections
A distinctive GPU-driven backdrop that signals technical craft before any copy is read.
Teaching uniform-driven shader animation
A compact, real example of updating a single float uniform per frame instead of animating geometry.
Game or interactive-experience landing pages
Pair with Three.js Scroll Shader Fluid Gradient Wave for a two-part shader showcase.
Related: Scroll Canvas Particle Text Formation
See Scroll Canvas Particle Text Formation for a Canvas2D scroll-driven alternative worth comparing against this WebGL shader.

Got questions?

Frequently Asked Questions

An OrthographicCamera(-1, 1, 1, -1, 0, 1) paired with a 2x2 plane makes the plane’s edges align exactly with the camera’s view volume at any aspect ratio, with no perspective-projection or distance-based sizing math needed. A PerspectiveCamera would require computing the exact plane size that fills the frustum at a chosen z-distance.

uProgress is a single float, updated every animation frame from a value kept in sync with a scrubbed GSAP ScrollTrigger tween. The fragment shader multiplies it into both the ripple’s frequency and its amplitude, so scrolling further into the pinned section makes the ripple visibly faster and more pronounced.

Because there is no image texture in this snippet — color comes from a procedural three-stop gradient mixed by smoothstep(). Distorting the UV coordinate before computing that gradient mix makes the gradient bands themselves visibly ripple; the same distorted-UV technique applies unchanged if you swap the gradient mix for a texture2D() sample.

uProgress is scroll-bound and only changes as the user scrolls (it can pause completely if scrolling stops), while uTime advances continuously from a THREE.Clock every frame regardless of scroll. The ripple’s phase depends on uTime so the surface keeps rippling even while the user holds still, while its intensity depends on uProgress so the ripple’s strength is still tied to scroll position.

Yes. Click JSX for a React component, Vue for a Vue 3 SFC, Angular for a standalone component, or Tailwind for a React + Tailwind version. Build the renderer, shader material, and GSAP ScrollTrigger inside a mount effect keyed to a canvas ref, and on unmount kill the ScrollTrigger instance, dispose the geometry and material, and call renderer.dispose().