Three.js Scroll Wave Terrain — GSAP Synthwave Grid
Three.js Scroll Wave Terrain · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
How to Build a Scroll-Driven Wave Terrain With Three.js and GSAP

The Three.js Scroll Wave Terrain snippet starts with a perfectly flat wireframe plane and raises it into drifting synthwave hills as the visitor scrolls — the scrollbar controls the wave amplitude directly, not a timer — using core Three.js vertex displacement and GSAP's ScrollTrigger plugin, both loaded from a CDN.
Scroll controls amplitude, time controls flow
The effect cleanly separates two inputs. A scroll-scrubbed value amp runs from 0 to 1 and multiplies the height of every hill, so scrolling literally raises the landscape out of flatness. Meanwhile a real-time clock t shifts the wave pattern forward every frame, so the hills always drift toward the camera regardless of scroll. Pausing the scroll freezes the *height* of the terrain but not its *motion* — the frozen hills keep rolling, which is what makes the scene feel alive rather than stuck.
Original vertex positions are kept, not overwritten
A PlaneGeometry with an 80×80 subdivision is rotated flat onto the ground, and a copy of its starting vertex array is stored once. Every frame the height field is recomputed from those *original* X/Z coordinates rather than from the last frame's displaced positions. This matters: if you displaced the already-displaced vertices, errors would accumulate and the terrain would drift into garbage within seconds. Reading from the pristine base each time keeps the surface mathematically stable no matter how long it runs.
Two overlaid sine waves for organic hills
A single sine wave produces obvious, repetitive corrugations. Layering two waves at different frequencies and axes — one along X, one mixing X and Z — creates interference that reads as natural, non-repeating rolling terrain. Both waves are scaled by the same scroll amplitude and animated by the same time value, so the whole surface rises and drifts as one coherent field.
The camera dips as the hills grow
To keep the composition working across the whole scroll, the camera's height eases downward as amp increases and its look target lifts slightly. When the terrain is flat the camera sits high looking across an empty plane; as the hills rise the camera drops so they fill the frame dramatically. Both camera moves derive from the same scrubbed amplitude, so they reverse perfectly on scroll-up.
Synthwave backdrop with fog and a neon sun
A CircleGeometry sun sits on the horizon and linear Fog fades the far edge of the plane into the purple gradient background, hiding where the finite grid ends and concentrating detail near the camera. The CSS background gradient and the fog color are matched so the WebGL canvas blends seamlessly into the surrounding scroll sections.
scrub: 0.5, pinned, fully reversible
A small numeric scrub smooths the amplitude against noisy input, and pinning for +=400% gives the terrain room to rise gradually. Because the height, camera dip, and look target all derive from one scrubbed value, scrolling back up flattens the landscape exactly. This same vertex-displacement approach powers the particle wave snippet; here it drives a solid grid instead of points. Pair it with a scroll tunnel or starfield warp for a full retro-futurist scroll journey.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You do not need to derive the math for stable animated terrain yourself. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to explain why the code stores the original vertex positions and re-reads them every frame, or why two overlaid sine waves look more natural than one. The same assistant can help you extend it — ask it to color the hills by height for a heat-map look, replace the sine waves with simplex noise for craggier terrain, or add a second, slower-moving ridge layer behind the first. It can also move the displacement into a vertex shader so the CPU stops rewriting the buffer each frame, which matters at higher subdivisions. Treat the code as a starting point for a conversation, 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:
Build a "scroll-driven wave terrain" in plain HTML, CSS, and JavaScript using Three.js, 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 with a WebGLRenderer and PerspectiveCamera, sized and updated on window resize including aspect ratio, plus linear Fog whose color matches the CSS background gradient.
- Create a THREE.PlaneGeometry about 40x40 units with an 80x80 subdivision, rotated flat onto the ground, rendered as a wireframe. Store a copy of its starting vertex position array once.
- Add a CircleGeometry "sun" disc on the horizon for a synthwave backdrop.
- Register a GSAP tween on a ScrollTrigger targeting the pinned section, with pin: true, start at top top, a small numeric scrub (~0.5), and an end several hundred percent tall, animating one plain value amp from 0 to 1.
- Every animation frame (requestAnimationFrame), recompute each vertex's height from the STORED original X/Z (never the displaced positions) as the sum of two sine/cosine waves at different frequencies, scaled by amp and animated forward by a real-time clock so the hills drift toward the camera; set needsUpdate.
- Ease the camera's Y downward and lift its look target as amp increases, both derived from amp.
- Confirm scrolling back up flattens the plane exactly, since amp is fully scrubbed rather than a one-way timer, and confirm the hills keep drifting even when scrolling is paused.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.
Step by step
How to Use
- 1Load all three CDN scriptsAdd three.min.js, gsap.min.js, and ScrollTrigger.min.js from the CDN panel, in that order.
- 2Paste HTML, CSS, and JSA flat neon wireframe grid appears under a synthwave sun in a pinned 3D stage.
- 3Scroll downThe grid rises into rolling hills that drift toward the camera as the camera dips lower.
- 4Scroll back upThe hills flatten back to a plane exactly, since the amplitude is fully scrubbed.
- 5Reshape the terrainEdit the two sine-wave frequencies and amplitudes to change the character of the hills.
- 6Tune the pacingAdjust the ScrollTrigger end value (+=400%) for a slower or faster rise of the landscape.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Scroll controls one value (amp) that scales hill height, while a real-time clock (t) shifts the wave pattern forward every frame. This lets the terrain keep drifting toward the camera even when scrolling is paused — the height freezes but the motion does not, which keeps the scene feeling alive instead of stuck at a scroll position.
A copy of the plane's starting vertex array is stored once, and each frame the height is computed from those pristine X/Z coordinates. If you displaced the already-displaced vertices instead, small errors would compound and the surface would drift into garbage within seconds. Reading the base every frame keeps the terrain mathematically stable indefinitely.
A single sine wave produces obvious, repetitive corrugations. Overlaying two waves at different frequencies and axes creates interference that reads as natural, non-repeating rolling hills. Both are scaled by the same scroll amplitude and animated by the same clock, so the whole surface rises and drifts as one coherent field.
The camera's height eases downward and its look target lifts as amp increases, both derived from the same scrubbed value. When the plane is flat the camera looks across an empty surface from above; as the hills grow the camera drops so they fill the frame. Because both moves derive from amp, they reverse exactly on scroll-up.
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 plane, store the base vertex array, and set up the GSAP timeline inside a mount effect against a canvas ref, and on cleanup kill the ScrollTrigger and call geometry.dispose() and renderer.dispose() so buffers and the WebGL context are freed on unmount.