Three.js Scroll Ocean Dive — GSAP Depth Color Grade
Three.js Scroll Ocean Dive · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
How to Build a Scroll-Driven Ocean Dive With Three.js and GSAP

The Three.js Scroll Ocean Dive snippet sinks the camera from just above an animated water surface down to a rocky sea floor 130 meters below, using GSAP's ScrollTrigger to scrub a single camera-Y value while a per-frame depth fraction drives fog color, light intensity, bubble visibility, and volumetric god rays. Where most scroll scenes travel forward along Z like the scroll tunnel, this one is a vertical journey — and almost all of its storytelling is done with color.
A two-stage color grade instead of one lerp
Water does not darken linearly: the plunge from air to water is an abrupt hue shift, then the fade to black is long and slow. The frame loop converts camera Y into a 0–1 depth fraction and splits it at 0.25 — the first quarter lerps the background and fog from sky blue to shallow teal, the remaining three quarters lerp from teal to near-black abyss. Both scene.background and scene.fog.color are copied from the same lerped color each frame, which is the critical trick: when fog matches the background exactly, distant geometry dissolves into "water" rather than fading against a mismatched backdrop.
The surface is a displaced plane you dive through
The water surface is a 64×64-segment plane whose vertices are re-displaced every frame with two offset sine waves, exactly like the height-field technique in the wave terrain snippet but rendered with side: THREE.DoubleSide at 85% opacity. That double-sided material matters: after the camera passes through it, the user looks up at the underside of the waves — a view a single-sided plane would simply cull away.
Bubbles rise while the camera sinks
Three hundred fifty points drift upward a fixed amount per frame and wrap back to the bottom once they cross the surface, following the recycle-not-respawn pattern from the starfield warp. Because the camera moves down while the bubbles move up, the relative velocity doubles the perceived descent speed at zero extra cost. Their material opacity snaps to zero while the camera is still above the waterline so no bubbles float in the sky.
God rays from additive planes, faded by depth
Seven tall planes with AdditiveBlending, near-zero opacity, and random Y rotations stand in for volumetric light shafts. Each ray's opacity is the product of a slow sine shimmer and (1 − depth), so the shafts wave gently in the shallows and die out completely before the abyss — matching the physical fact that sunlight does not reach deep water. The directional light's intensity is scaled by the same factor, dimming the sea floor until the fog color provides most of what you see.
One scrubbed value, everything else derived
The GSAP tween scrubs only dive.y from +14 to −116 with ease: 'none'. Depth fraction, both color lerps, fog density, sun intensity, bubble opacity, ray fade, and the live DEPTH HUD all derive from that value inside requestAnimationFrame, so scrolling back up resurfaces through the same color grade in reverse, with no one-shot triggers to desynchronize. Surface waves, camera sway, and bubble wobble run on THREE.Clock time so the ocean keeps moving while the scroll is idle — the same scrubbed-primary, clock-secondary split used across this series, including the planet approach.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You do not need to work out the underwater color science yourself. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to explain the two-stage fog grade, why fog color must equal background color, or how the god rays fake volumetric light with additive planes. The same assistant can extend the dive — adding a school of fish as an instanced mesh weaving past the camera, feature-callout HTML pinned at specific depths, a pressure gauge that accelerates with depth, or caustic light patterns on the sea floor from an animated texture. It can also retheme the whole scene to a brand palette by regenerating the three grade colors. Treat the code as a starting point to question and reshape, 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-scrubbed ocean dive" in plain HTML, CSS, and JavaScript using Three.js and GSAP's ScrollTrigger plugin, all loaded from a CDN (no bundler, no build step).
Requirements:
- A pinned full-viewport section with a canvas, WebGLRenderer, and PerspectiveCamera resized (with aspect) on window resize.
- A water surface from a 64×64-segment PlaneGeometry rotated flat, displaced every frame by two offset sine waves, rendered with a DoubleSide MeshLambertMaterial at ~85% opacity so the camera can pass through and look back up at it.
- One GSAP tween (ease "none") scrubbing camera Y from ~+14 to ~−116 on a ScrollTrigger with pin: true and end around +=500%.
- Each frame derive a 0–1 depth fraction from camera Y and run a TWO-STAGE color grade: lerp sky→shallow-teal across the first quarter, teal→near-black across the rest, copying the result into BOTH scene.background and scene.fog.color, while also raising fog density and dimming a DirectionalLight by depth.
- 300+ bubble points that rise a fixed amount per frame and wrap from the surface back to the bottom by mutating one Float32Array in place; hide them (opacity 0) while the camera is above the waterline.
- 5–8 god-ray planes: tall PlaneGeometry with AdditiveBlending, depthWrite false, opacity ~0.06, random Y rotation, each fading by (1 − depth) times a slow sine shimmer.
- A sea floor at the bottom: a plane displaced into dunes with sin/cos, computeVertexNormals called once, plus ~24 randomly rotated DodecahedronGeometry rocks.
- A live "DEPTH n M" HUD derived from camera Y, an intro overlay that fades once the dive starts, and clock-driven camera sway so the scene moves while scroll is idle.
- Confirm scrolling back up resurfaces through the same color grade in reverse — every visual state must derive from the scrubbed Y and the clock, with no one-shot events.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 the three CDN scriptsAdd three.min.js, gsap.min.js, and ScrollTrigger.min.js in that order before the snippet JS.
- 2Paste HTML, CSS, and JSA pinned view hovers above animated waves under a sky-blue background, with a DEPTH 0 M HUD in the corner.
- 3Scroll to plungeThe camera passes through the double-sided surface — background and fog snap through the sky-to-teal stage of the color grade.
- 4Keep scrolling into the deepGod rays shimmer and fade, bubbles stream upward past the camera, light dims, and the teal-to-abyss lerp takes over.
- 5Reach the sea floorDisplaced dunes and scattered dodecahedron rocks emerge from the fog at around 130 m as the scrub completes.
- 6Retune the gradeChange the SKY, SHALLOW, and ABYSS colors or the 0.25 stage split to restyle the whole dive — every fade derives from those three constants.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
A single lerp spends most of the scroll in muddy mid-tones and makes the water entry feel gradual, which reads as fog rather than a plunge. Splitting at depth fraction 0.25 gives an abrupt sky-to-teal shift across the surface crossing, then a long teal-to-black fade below — matching how light actually behaves in water and making the moment you submerge unmistakable.
FogExp2 blends geometry toward the fog color with distance. If that color matches the background, distant objects dissolve seamlessly into "water" and the scene reads as a continuous volume. If the two colors differ even slightly, every mesh gets a visible halo where fogged pixels meet the backdrop, instantly breaking the underwater illusion. Both are copied from the same lerped THREE.Color each frame.
Seven tall PlaneGeometry meshes with AdditiveBlending, depthWrite: false, and opacity around 0.06 are fanned at random rotations below the surface. Additive blending means overlapping rays brighten each other like real light shafts, and each ray's opacity is multiplied by (1 − depth) plus a slow sine so they shimmer in the shallows and vanish before the abyss. It is a fake, but at these opacities the eye accepts it completely.
Relative motion is what the eye measures. Bubbles rising at a fixed rate while the camera sinks doubles the perceived descent speed without shortening the scroll distance or increasing scrub speed. They wrap from surface back to −120 in place — one Float32Array updated per frame, one draw call, no allocation — the same recycling used by the starfield warp snippet.
Yes. Export via the JSX, Vue, Angular, or Tailwind buttons. Mount the scene in useEffect / onMounted / ngAfterViewInit against a canvas ref, and on teardown kill the ScrollTrigger, dispose the surface, floor, bubble, and ray geometries and materials, and call renderer.dispose(). The surface-wave base positions are captured with slice() at build time, so keep that copy inside the effect scope.