webdevpuneet.com

Scroll-Triggered GLB Model Carousel — Free GSAP ScrollTrigger + Three.js Gallery Snippet

Scroll-Triggered GLB Model Carousel · Scroll · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<section class="gcs-intro"><h1>Scroll through a real gallery of glTF models</h1><p>Three real .glb models slide through one turntable as you scroll — drag to orbit whichever one is currently on stage.</p></section>
<section class="gcs-pin" id="gcsPin">
  <canvas id="gcsCanvas"></canvas>
  <div class="gcs-dots" id="gcsDots">
    <span class="gcs-dot active" data-i="0"></span>
    <span class="gcs-dot" data-i="1"></span>
    <span class="gcs-dot" data-i="2"></span>
  </div>
  <div class="gcs-label" id="gcsLabel">Duck</div>
  <div class="gcs-hint">Drag to orbit · plain scroll always scrolls the page · Ctrl/Cmd + scroll to zoom</div>
  <div class="gcs-zoom">
    <span class="gcs-zoom-label">+</span>
    <input type="range" id="gcsZoom" class="gcs-zoom-slider" min="0" max="100" step="1" />
    <span class="gcs-zoom-label">&minus;</span>
  </div>
</section>
<section class="gcs-outro"><p>End of the gallery &mdash; scroll back up to bring the first model back on stage.</p></section>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Three real glTF binary models
Loaded via THREE.GLTFLoader into a genuine multi-model gallery, not one model recolored.
Scroll-scrubbed turntable rotation
The whole gallery group's rotation is a direct, reversible function of scroll progress.
Every model stays loaded
No show/hide toggling or reloading when scrolling back to a model already seen.
Independent per-model fallbacks
Each model's own load failure swaps in its own placeholder, in its own slot only.
Live label and dot indicator
A real model name and a synced dot row track the current scroll-selected model.
Simultaneous manual orbit
OrbitControls dragging works at all times, with zero conflict handling.
Auto-fit model scale
Every model is scaled to the same target height via bounding-box measurement.
Slider + Ctrl/Cmd-scroll zoom
Zoom is an explicit, opt-in gesture, never a hijacked plain scroll wheel.

About this UI Snippet

Scroll-Triggered GLB Model Carousel — A Real Multi-Model Gallery on One Turntable

Screenshot of the Scroll-Triggered GLB Model Carousel snippet rendered live

Showing off more than one 3D model on scroll usually means either loading them one at a time (with an awkward swap moment) or building a full multi-scene setup. This snippet does neither: it loads three genuinely different .glb models onto three slots of a single rotating turntable Group, and lets scroll rotate that whole group — bringing each model into the camera's fixed viewing position in turn, purely by rotation.

One turntable, three slots, not three toggled visibilities

GALLERY is loaded once into three child Groups (slots), each positioned 120 degrees apart around a shared turntable group's center. Every model stays loaded and present in the scene at all times — there's no show/hide toggle, no re-parenting, and no re-triggering a load when scrolling back to a model you've already seen. Only turntable.rotation.y ever changes.

Scroll rotates the turntable, never an individual model

ScrollTrigger's onUpdate maps the 0-1 scroll progress across (TOTAL_SLOTS - 1) steps and sets turntable.rotation.y directly from that value — a pure, reversible function of scroll position, exactly like the single-model scroll-scrubbed GLB turntable snippet, just applied to which model faces the camera instead of to one model's own spin.

Camera-space orbit, object-space carousel — still zero conflict

OrbitControls only ever repositions the camera around a fixed target point. The scroll handler only ever sets the turntable group's own rotation. Because those are two different transforms on two different objects, dragging to orbit whichever model currently faces the camera and scrolling to bring the next model into view both work simultaneously with no coordination code, exactly the same conflict-free pattern this whole snippet family relies on.

Independent, honest per-model fallbacks

Each of the three GLTFLoader.load() calls has its own error callback that names which gallery entry failed and swaps in a color-matched placeholder sphere into that specific slot — so one broken model URL only ever affects its own position on the turntable, never the other two.

A live label and dot indicator, not just a number

Alongside the numeric scroll progress, the current step is rounded to the nearest whole model index and used to update both a text label (the model's actual name) and a row of dot indicators — the same UI pattern real image/product carousels use, applied here to a genuinely 3D, scroll-scrubbed gallery.

Zoom is opt-in, not a hijacked scroll wheel

OrbitControls' built-in wheel-zoom is turned off, with zoom reimplemented as a slider plus Ctrl/Cmd + scroll, so a plain scroll always advances the page.

Customizing it

Add a fourth entry to GALLERY (the slot math adapts automatically), change SLOT_RADIUS for a tighter or wider turntable, or pair this with GLB model comparison viewer for a non-scroll-driven way to look at two models at once.

Step by step

How to Use

  1. 1
    Add all five CDN scriptsthree.min.js, GLTFLoader.js, OrbitControls.js, gsap, and ScrollTrigger.
  2. 2
    Paste HTML, CSS, and JSThe Duck loads facing the camera; the other two models wait on the turntable.
  3. 3
    Scroll into the pinned sectionThe turntable rotates smoothly, bringing each model into view in turn.
  4. 4
    Watch the label and dotsBoth update live to show which model currently faces the camera.
  5. 5
    Drag on the canvas at any pointOrbit freely around whichever model is currently on stage.
  6. 6
    Use the slider or Ctrl/Cmd + scroll to zoomPlain scroll always advances the page; zoom is a separate, opt-in gesture.
  7. 7
    Add another modelPush a new { name, url, color } entry into GALLERY — the slot layout adapts automatically.

Real-world uses

Common Use Cases

Multi-product showcases
Walk a visitor through several products on one rotating turntable, with all three real glTF models staying loaded so nothing reloads when scrolling back.
Portfolio and case study reels
Present multiple 3D assets in one scene, with the whole group's rotation a pure function of scroll progress.
glTF and ScrollTrigger teaching
Study a complete real example of multi-model loading, with each model having its own placeholder fallback if its file fails.
Rotating museum exhibits
Bring several artefacts into one rotating museum exhibit, comparing against the single-model scroll GLB duck turntable for contrast.
Comparison companions
Pair with the GLB model comparison viewer so visitors can both browse models by scrolling and compare two side by side.
Related: Scroll-Scrubbed GLB Motion Trail
See the Scroll-Scrubbed GLB Motion Trail for a related scroll pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

All three models are loaded once into their own child Group, each positioned 120 degrees apart around a shared parent turntable Group's center. Scroll only ever rotates that parent turntable group's own rotation.y property — every model stays loaded, present, and untouched in the scene at all times, so there's no show/hide toggle, re-parenting, or re-triggered load involved in bringing a different model into the camera's fixed viewing position.

They act on two different transforms. OrbitControls repositions the camera around a fixed target point — camera-space. The scroll handler sets the turntable group's own rotation.y directly — object-space, and specifically the whole gallery's rotation, never any individual model's own local transform. Because a visitor's drag and the scroll handler never write to the same property, both work simultaneously with zero conflict-resolution code, the same pattern used throughout this snippet family.

Each of the three GLTFLoader.load() calls has its own independent error callback that logs specifically which gallery entry failed and adds a color-matched placeholder sphere into that model's own slot only — the other two models continue to load and rotate into view completely normally, so a single broken URL never breaks the whole carousel.

Push a new { name, url, color } object into the GALLERY array. The slot positions are computed automatically by dividing a full circle by GALLERY.length, and the ScrollTrigger's scroll-distance and rotation-per-step math both read from GALLERY.length as well, so adding an entry automatically adjusts the turntable spacing and how far you need to scroll to see every model.

Set up the renderer, scene, GLTFLoader calls, OrbitControls, and the turntable/slots group hierarchy inside a mount effect, keeping GALLERY and the turntable group in refs so the ScrollTrigger onUpdate callback can reach current values. 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.