UIUX Learning

Building a 3D vinyl-shop landing that scrolls as one continuous animation

Jun 21, 2026 · 4 min read
The three acts of WAX & SOUL (real render). Left: the spinning hero; right: the New Arrivals carousel and the final drop onto the turntable.

I wanted to try one thing: could a single scroll carry one continuous 3D animation from top to bottom? Not a stack of separate sections, but one record for the whole page — as you scroll, it spins, glides, and drops onto a turntable. I used a fictional record store, “WAX & SOUL,” as the subject, with Three.js procedurally generating the record, sleeves and a wooden turntable, and Lenis for smooth scroll.

👉 See the live page here — scroll from top to bottom to see the whole animation.

One scroll = one animation

A traditional landing page is built section by section, each with its own entrance animation. This time I wanted the opposite: one camera, one timeline. The scroll progress (from 0 to 1) is the timeline — I used no timeline library, just took scrollTop / maxScroll as a 0→1 value p, split the three acts with a few eased segments (smoothstep), and on every frame lerped each object’s position, rotation and the camera from p. No keyframes, no GSAP — just maths.

The payoff: wherever the scroll is, the frame is guaranteed to match that state, forwards or backwards. To debug a scroll animation, you can jump straight to any p and inspect the result.

The three acts

Act 1 · Hero — A record sits centred (slightly high) and spins continuously, with the store tagline and a scroll ↓ cue below. It looks like just a spin, but it only reads as real because of the procedurally generated grooves (concentric ridges) and the label’s specular highlight.

Act 2 · New Arrivals — The hero record doesn’t vanish; it glides down into the first New Arrivals album slot and its spin eases to a stop — the same record carried from act one ties the narrative together. From there, scrolling pans the camera across five albums with different cover colours: the centre one pulls in and scales up while the neighbours recede. The centre album also pulls its record out in two beats (sleeve tilts, record slides out, moves forward and centres).

Act 3 · Turntable — The last record flips flat (rotation.x → -90°) and drops onto a retro wooden turntable; the camera tilts down at the same time, the tonearm swings in from the top-right and cues onto the outer edge, and the record spins on the platter. It closes with “Find your sound.” and a Browse CTA.

What I learned

1. Driving everything from scroll progress saves a lot. No timeline library, no play / pause / reverse to manage — scroll back up and the animation simply reverses. State is fully determined by p with no hidden state, so you never get the “scrolled too fast, the trigger didn’t fire” class of bug.

2. Procedural 3D is cheap and flexible. The record’s grooves, label, cover colours and wood grain are all drawn live as canvas textures — no 3D models or image maps downloaded. Want a different album colour? Change one hex — the five records are the same geometry recoloured.

3. You need a fallback. If WebGL can’t start (old machine / hardware acceleration off), the canvas is hidden and a pure-CSS static record plus the store name is shown — no blank screen. On narrow screens the camera pushes back by aspect ratio so the whole record still fits.

4. Balancing smooth and snap is hard. I first used hard snapping (one flick locks to the next act), but the scroll kept getting hijacked mid-gesture and felt awful. The fix was free Lenis smooth scroll plus a gentle snap-to-nearest-anchor only once you stop — so you can dwell on a transition if you want, and it only aligns when you let go.

Takeaway

From one sentence — “carry a single continuous 3D animation with scroll alone” — to a record that spins, glides, runs a carousel and drops onto a turntable, the biggest lesson is this: once you bind the animation’s timeline to scroll, the experience shifts from “reading a page” to “holding a camera.” The viewer controls the pace, yet every frame still lands where it should.