Scroll-Scrubbed GLB Turntable — Free GSAP ScrollTrigger + Three.js glTF Snippet

Scroll-Scrubbed GLB Turntable · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Real glTF binary model
Loaded via THREE.GLTFLoader from an actual .glb file, not a primitive shape.
Scroll-scrubbed object rotation
duck.rotation.y is a direct, reversible function of scroll progress.
Simultaneous manual orbit
OrbitControls dragging works at all times, with zero conflict handling.
Two independent transforms
Camera-space orbit and object-space spin never touch the same property.
Async-safe scroll handling
onUpdate checks the model exists before touching it, so early scroll is safe.
Honest load-failure fallback
A logged error swaps in a placeholder sphere instead of a silent void.
Slider + Ctrl/Cmd-scroll zoom
Zoom is an explicit, opt-in gesture, never a hijacked plain scroll wheel.
Studio-lit scene
Key/fill lighting and a grounding disc, matching this library's 3D conventions.

About this UI Snippet

Scroll-Scrubbed GLB Turntable — A Real glTF Model, Spun by Scroll, Orbitable by Hand

Screenshot of the Scroll-Scrubbed GLB Turntable snippet rendered live

Most "3D on scroll" demos either fake the model with a primitive shape or lock the camera so tightly to the scroll animation that a visitor can't actually look at the thing from their own angle. This snippet does neither: it loads a genuine .glb file with Three.js's GLTFLoader, spins that model's own rotation directly from scroll position via GSAP ScrollTrigger's scrub, and keeps full OrbitControls dragging active the entire time — so scrolling and manually orbiting the camera both work, simultaneously, without a single line of conflict-resolution code.

Two independent transforms, not one shared one

The trick that makes drag-to-orbit and scroll-to-spin coexist for free is that they never touch the same property. OrbitControls moves the camera around a fixed target point — that's camera-space. The scroll handler sets duck.rotation.y directly on the model — that's object-space. A visitor dragging the canvas is orbiting the camera around the duck; scrolling is spinning the duck itself. Both are true simultaneously, and because they're different transforms on different objects, there's nothing to arbitrate — no "pause auto-rotate while dragging" logic needed at all, unlike patterns where scroll and drag would fight over the same camera position.

A real glTF binary, not a placeholder mesh

new THREE.GLTFLoader().load(MODEL_URL, ...) fetches Khronos' own official sample-asset Duck model — a real .glb binary containing mesh geometry, UVs, and a baked texture — from its GitHub repository via the jsdelivr CDN, no asset hosting of your own required. The load is async: the duck variable starts null, and the ScrollTrigger's onUpdate callback checks for that before touching .rotation, so scrolling before the model finishes loading never throws.

Scrubbed, not animated on a timer

ScrollTrigger's onUpdate receives a progress value from 0 to 1 representing exactly how far through the pinned scroll range the user currently is, and duck.rotation.y is set to progress * Math.PI * 2 * TOTAL_TURNS — a direct, reversible function of scroll position. There's no requestAnimationFrame tween running on its own clock; scroll back up by one pixel and the model rotates back by the exact corresponding fraction of a turn.

A named, honest fallback if the CDN model fails

The loader's third argument is an error callback. If the .glb genuinely fails to fetch (a blocked network, an offline preview), the snippet logs the real error and swaps in a plain sphere so the scene is never a silent empty void — the same shape-matched-fallback discipline used throughout this library for anything that depends on an external resource.

Zoom is opt-in, not a hijacked scroll wheel

OrbitControls' built-in wheel-zoom is deliberately turned off (controls.enableZoom = false) — left on, it calls preventDefault() on every wheel event over the canvas, silently swallowing the page scroll this whole demo depends on. Zoom is reimplemented as two explicit, opt-in gestures instead: a vertical slider next to the canvas, and Ctrl/Cmd + scroll wheel (the same convention embedded Google Maps and most map widgets use). Both call the same setZoomDistance() helper, which clamps to controls.minDistance/maxDistance and keeps the slider's position in sync no matter which input triggered the change.

Customizing it

Swap MODEL_URL for any other Khronos sample-asset `.glb` or your own hosted model, change TOTAL_TURNS for a faster or slower spin, or combine this pattern with scroll reveal grid for a longer scroll narrative. Pair it conceptually with three product viewer, which uses the same OrbitControls setup on a procedural (non-glTF) shape.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why setting the model's rotation.y directly from ScrollTrigger's progress value produces motion that scrubs correctly in both directions, and why OrbitControls dragging and the scroll-driven spin never need explicit conflict-resolution code (hint: they act on different transforms — camera-space versus object-space). It's also useful for extending the demo — ask it to drive a second property (like scale or a material's emissive intensity) from the same scroll progress, add scroll-triggered checkpoints that pause the spin at specific named angles, or swap in a model with its own baked animation clip and scrub that instead of a simple Y rotation. Use the conversation to build real intuition for scroll-scrubbed 3D before adapting the technique to your own glTF model.

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-scrubbed GLB turntable" in plain HTML, CSS, and JavaScript using Three.js (core, GLTFLoader, and OrbitControls, all loaded from a CDN with no bundler) plus GSAP with its ScrollTrigger plugin.

Requirements:
- A pinned full-viewport Three.js scene (GSAP ScrollTrigger pin: true) with an intro section before it and an outro section after, studio-lit with at least a key and fill directional light plus a simple ground/grounding element.
- Load a real .glb model using THREE.GLTFLoader pointed at a genuine, freely-licensed, CDN-hosted glTF binary URL (e.g. one of Khronos' official glTF-Sample-Assets models) — do not substitute a Three.js primitive geometry standing in for "a model."
- Set up OrbitControls on the camera with damping enabled, so the user can click-and-drag the canvas at any time to orbit the camera and inspect the model from any angle — this must work continuously, not be disabled during the scroll-driven animation.
- Use ScrollTrigger's scrub option (not a plain trigger/timer) so that a progress value from 0 to 1 is available every frame the pinned section is being scrolled through, and set the loaded model's own rotation.y directly from that progress value multiplied by 2*PI times some number of full turns — the rotation must be a pure, reversible function of scroll position, not a one-shot animation.
- Ensure the scroll-driven model rotation and the OrbitControls camera-drag orbiting can both be used at the same time with zero explicit conflict-resolution code, by making sure they modify different transforms (the model's own rotation versus the camera's position/orbit) rather than fighting over the same property.
- Handle the model still being asynchronously in-flight when scroll events first fire (don't throw if the mesh hasn't loaded yet), and handle the GLTFLoader's error callback by logging the real error and substituting a simple placeholder mesh so the scene is never blank if the model fails to load.
- Display a small live label showing the current rotation progress (e.g. "1.3 turns") derived from the same scroll progress value.

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

  1. 1
    Add all five CDN scriptsthree.min.js, GLTFLoader.js, OrbitControls.js, gsap, and ScrollTrigger — in that order.
  2. 2
    Paste HTML, CSS, and JSThe duck model loads and appears facing forward, unrotated.
  3. 3
    Scroll into the pinned sectionThe duck spins on its own Y axis in direct proportion to scroll position.
  4. 4
    Drag on the canvas at any pointThe camera orbits freely — scroll-driven spin keeps working underneath.
  5. 5
    Use the slider or Ctrl/Cmd + scroll to zoomPlain scroll always advances the page; zoom is a separate, opt-in gesture.
  6. 6
    Scroll back upThe duck's rotation reverses exactly, since it's a pure function of progress.
  7. 7
    Swap the model URLPoint MODEL_URL at any other .glb to spin a different object.

Real-world uses

Common Use Cases

Product and portfolio scroll stories
A genuinely 3D centerpiece for a scroll-driven product reveal section.
glTF/GLTFLoader teaching demos
A minimal, complete example of loading and animating a real .glb file.
Brand and agency showpieces
Demonstrates combined GSAP ScrollTrigger + Three.js technical range.
Museum/collectible scroll exhibits
Spin an artifact model into view as part of a longer scroll narrative.
Alongside other 3D snippets
Compare against three product viewer's time-driven auto-rotate.
Scroll-narrative sequences
Combine with scroll reveal grid sections before or after.

Got questions?

Frequently Asked Questions

They act on two completely different transforms. OrbitControls repositions the camera around a fixed target — camera-space. The scroll handler sets the model's own rotation.y directly — object-space. Since a visitor's drag and the scroll handler never write to the same property, there is nothing to arbitrate: both simply happen, simultaneously, all the time. Zoom follows the same discipline: OrbitControls' own wheel-zoom is turned off so a plain scroll always advances the page, and zooming is reimplemented as an explicit slider plus Ctrl/Cmd + scroll.

A real one — THREE.GLTFLoader fetches and parses an actual .glb binary (Khronos' official sample-asset Duck model) containing real mesh geometry, UV coordinates, and a baked texture, the same file widely used across Three.js GLTFLoader tutorials and demos.

The loader's error callback logs the real failure to the console and swaps in a plain sphere mesh, so the scene never renders as a silent empty void. This mirrors the honest-fallback pattern used throughout this snippet library for anything depending on an external resource.

Setting rotation directly from ScrollTrigger's progress value (0 to 1) makes the spin a pure, reversible function of scroll position — scrolling back up by any amount rotates the model back by the exact corresponding fraction of a turn. A timer-based animation would only ever play forward and couldn't scrub.

Set up the renderer, scene, GLTFLoader call, and ScrollTrigger inside a mount effect, storing the loaded model and controls in refs so the onUpdate callback can reach them. Call controls.dispose(), renderer.dispose(), and the ScrollTrigger instance's .kill() in the cleanup function to release the WebGL context and remove the pin/scroll listeners on unmount.