Three.js Scroll Rubik’s Cube Assembly — GSAP Scrub
Three.js Scroll Rubik’s Cube Assembly · Scroll · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
How to Build a Scroll-Scrubbed Rubik’s Cube Assembly With Three.js and GSAP

The Three.js Scroll Rubik’s Cube Assembly snippet scatters 27 colored cubelets across a wide spherical cloud and lets scroll pull them into a solved 3×3×3 Rubik’s cube, finishing with a turntable presentation spin — all scrubbed through one GSAP ScrollTrigger value with a hand-rolled, fully reversible stagger. It shares DNA with the particle assembly snippet but works at mesh scale: 27 solid boxes with per-face materials rather than thousands of points, which makes material choices and stagger design the interesting problems.
Per-face materials so the interior reads as plastic
A real Rubik’s cube only shows color on outward faces; the interior is black plastic. Each BoxGeometry cubelet receives an array of six materials, and a face gets its classic color only when the cubelet sits on that face of the 3×3×3 grid (x === 1 gets red, y === 1 gets white, and so on) — every other side shares one dark material instance. This matters mid-flight: as scattered cubelets tumble toward home you see their dark interior sides, and when the cube closes up, the thin dark gaps between stickers appear automatically, with no texture work.
Deterministic scatter from a seeded hash
Scatter positions come from a tiny sin-hash function (Math.sin(seed × 127.1 + 311.7) × 43758.5453, take the fraction) instead of Math.random(). Each cubelet derives its scatter direction, radius, and tumble spin from its index, so the cloud is identical on every page load. That determinism is what keeps the scrub honest: scroll down, up, and down again and every cubelet retraces exactly the same flight path, something unseeded randomness regenerated on any rebuild would silently break.
A hand-rolled stagger that scrubs in both directions
GSAP timelines offer stagger, but this snippet computes stagger manually so everything stays a pure function of one scrubbed value: each cubelet's local progress is (build − delay) / (1 − 0.35) clamped to 0–1 and shaped by an ease-out cubic, where delay is proportional to the cubelet's distance from the cube center. The result is a cube that grows from its core outward — the center cubelet snaps in first, corners arrive last — and reverses into an outward explosion when scrolling up. Position uses lerpVectors(scatter, home, lp) while rotation multiplies the tumble spin by (1 − lp), so every cubelet lands at exactly zero rotation.
A phase split: 80% build, 20% presentation
The scrubbed progress is split into two windows, the same pattern as the staging in product stages: the first 80% drives assembly, the final 20% drives a full 360° turntable rotation plus a 5-unit camera dolly-in. Because present is derived (not a separate tween), the handoff point cannot drift, and reversing through it un-spins the cube before disassembling it.
A live percentage from arrival counting
The HUD counts cubelets whose local progress exceeds 0.985 and shows the fraction as a percentage. Counting arrivals rather than echoing raw scroll progress makes the number honest — it sits at 0% while everything is still tumbling, climbs as pieces click in center-out, and reaches 100% just as the presentation spin begins, giving the user a satisfying sense of completion mechanics similar to the gate counter in the portal gate sequence.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You do not need to derive reversible stagger math from scratch. Paste this snippet's HTML, CSS, and JS into an AI assistant like Claude and ask it to explain why the scatter uses a seeded sin-hash, how the per-cubelet delay creates a center-out build, or why rotation is multiplied by (1 − progress) instead of tweened separately. The same assistant can extend the scene — making the assembled cube perform actual face turns after assembly, swapping the six colors for your brand palette while keeping the dark interior, scaling up to a 4×4×4 with delays recomputed automatically, or adding a click handler that re-explodes the cube with gsap.to on the same progress object. Treat the code as a starting point to interrogate 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 Rubik's cube assembly" 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; ambient plus one directional light.
- 27 BoxGeometry cubelets in a 3×3×3 grid (size ~1.9, gap ~0.12). Each cubelet takes an ARRAY of six materials: the classic face color only when the cubelet lies on that outer layer (x=+1 red, x=−1 orange, y=+1 white, y=−1 yellow, z=+1 green, z=−1 blue), all other faces sharing one dark plastic material instance.
- Deterministic scatter: derive each cubelet's scatter direction, radius (~16–34), and tumble spin vector from its index via a seeded sin-hash (fract(sin(seed*127.1+311.7)*43758.5453)) — no Math.random — so every load and every scrub reversal retraces identical paths.
- One GSAP tween (ease "none") scrubbing a progress value 0→1 on a ScrollTrigger with pin: true and end ~+=400%.
- Hand-rolled stagger in the frame loop: each cubelet's delay is proportional to its home distance from the center; local progress = clamp((build − delay)/0.65) shaped by ease-out cubic; position = lerpVectors(scatter, home, lp); rotation = spin × (1 − lp) so pieces land at exactly zero rotation. The cube must visibly grow center-out.
- Phase split derived from the same value: first 80% assembles, last 20% spins the whole group a full 2π turntable while the camera dollies in ~5 units.
- A HUD showing percent assembled by COUNTING cubelets whose local progress exceeds 0.985, plus an intro overlay that fades once scrolling starts and a slow clock-driven idle rotation.
- Confirm scrolling back up un-spins the cube then explodes it outward along the identical flight paths.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 shows 27 colored cubelets tumbling in a slow-rotating scattered cloud with a 0% ASSEMBLED HUD.
- 3Scroll to assembleCubelets fly home center-out — the core snaps in first, corners last — while each one untumbles to land at zero rotation.
- 4Watch the finishPast 80% progress the completed cube performs a full turntable spin as the camera dollies in five units.
- 5Scroll back upThe cube un-spins, then explodes outward along the exact same flight paths — the seeded scatter makes reversal pixel-identical.
- 6Restyle itSwap the six FACE colors for a brand palette, or change STEP and the delay multiplier to alter cube tightness and stagger spread.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The scrub must be a pure function of scroll position: scrolling down, up, and down again has to retrace identical flight paths. Math.random() would generate a new cloud on every reload and make per-cubelet phase relationships unstable. The sin-hash (fract of Math.sin(seed × 127.1 + 311.7) × 43758.5453) gives each index a fixed pseudo-random direction, radius, and spin — random-looking, but permanent.
GSAP staggers offset start times inside a playing timeline, but this scene is scrubbed, not played. Instead each cubelet stores a delay proportional to its distance from the cube center, and every frame computes local progress as (build − delay) / 0.65, clamped and eased. That makes stagger a pure function of the single scrubbed value, so it reverses perfectly — corners leave first on the way back out.
A real Rubik's cube is black plastic with colored stickers on outward faces only. Passing a material array to a Mesh assigns one material per BoxGeometry face group: a cubelet gets red only if it sits on the x = +1 layer, white only on y = +1, and so on, with all interior faces sharing a single dark material instance. Mid-flight tumbles show dark undersides, and the assembled cube shows sticker-gap lines for free.
Position uses lerpVectors(scatter, home, lp) which hits home exactly at lp = 1, and rotation is spin × (1 − lp), which reaches exactly zero at the same moment. Since the ease-out cubic maps input 1 to output 1 precisely, there is no residual drift to snap or correct — arrival is mathematically exact, not approximated with a tolerance.
Yes. Use the JSX, Vue, Angular, or Tailwind export buttons. Build the cubelet loop and ScrollTrigger inside a mount effect against a canvas ref; on cleanup kill the ScrollTrigger, dispose each BoxGeometry and every non-shared face material (dispose the shared dark material once), and call renderer.dispose() so the pin and WebGL context release on unmount.