AETumi — AI-native 3D web platform for Three.js, WebGL and interactive websites
News & Guides

How to Build Scroll-Driven 3D Animation with Three.js

September 9, 2026 · AETumi

Key answer: scroll-driven 3D animation with Three.js works by making the scene a pure function of one normalized progress value. Read the scroll position, divide it by the maximum scrollable distance to get a value from 0 to 1, ease it, and let the render loop drive the camera, objects and materials from that number each frame. For a single moving scene, map scroll manually; for pinned, multi-section storytelling, add GSAP ScrollTrigger to own the pin and scrub. Keep the canvas a decorative layer over crawlable HTML, cap devicePixelRatio, render on demand, and honor reduced motion. This guide builds the scroll-to-progress pipeline step by step, adds GSAP for pinning, and covers the performance, SEO and accessibility that make it production-ready.

Table of contents

What scroll-driven 3D animation is

Scroll-driven 3D animation is the technique where scroll position is the input to a real-time Three.js scene. As the visitor scrolls, the browser reports how far down the page they are; you convert that to a value between 0 and 1 and use it to interpolate the scene — the camera flies, a model rotates, a material dissolves. The document still scrolls; the difference is that a render loop reads the scroll each frame and repaints a canvas.

From idea to productionAETumi technical diagram — From idea to productionServer-render contentLazy-load 3D bundleAdapt DPR & qualityDispose on route changeShip fast
From idea to production
AESPORT — High-Performance 3D Sports & Sneaker Website
AESPORT — High-Performance 3D Sports & Sneaker Website — live preview from the AETumi library

The mental model that keeps this maintainable is that the canvas is a presentation layer, not the page. Your headings, copy and links stay real HTML. That separation is what lets scroll-driven 3D stay fast, indexable and accessible while feeling cinematic, and it is the first principle across the 3D scroll hub.

The one-value architecture

Every robust scroll-driven Three.js animation reduces to one flow: scroll produces a normalized progress value, that value feeds an eased timeline, and the render loop consumes it to update the scene. One number bridges the DOM and the WebGL scene.

AETumi live WebGL scene — real-time Three.js rendered in your browser, not a video.

Keeping progress as a single normalized value is the whole trick. It makes the system predictable — you can log it, scrub it, and jump to any moment by setting it directly — and it decouples the animation from exact page height, so responsive layouts do not shatter the timing. Every animation becomes a pure function of one input, the architecture that separates a scene that ships from a demo that breaks on resize, and the foundation of scroll work on Three.js.

One connected systemAETumi technical diagram — One connected system3D WEBSTACKNext.jsR3FThree.jsWebGLGSAPAssets
One connected system

Set up the renderer

Start with a capped renderer. The device-pixel-ratio cap is the single most important line for scroll performance — uncapped retina rendering is the usual reason a scroll scene stutters on real devices.

import * as THREE from 'three'
const canvas = document.querySelector('#scene')
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true })
renderer.setPixelRatio(Math.min(devicePixelRatio, 2))    // cap: never full retina
renderer.setSize(innerWidth, innerHeight)
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100)
camera.position.set(0, 0, 6)

At devicePixelRatio 3 you render nine times the pixels of ratio 1. Capping to 2 cuts fill-rate dramatically with almost no visible quality loss on a moving scene, protecting the whole experience before you draw anything.

Map scroll to a 0 to 1 value

This mapping is the core of scroll-driven animation. Read the scroll, normalize it against the maximum scrollable distance, and smooth it so the scene never snaps.

let target = 0, progress = 0
function readScroll() {
  const max = document.documentElement.scrollHeight - innerHeight
  target = max > 0 ? scrollY / max : 0
}
addEventListener('scroll', readScroll, { passive: true }); readScroll()
// in the render loop: exponential smoothing
progress += (target - progress) * 0.08

The progress value eases toward the real scroll position. The 0.08 factor is the weight of the scene — lower is heavier and more cinematic, higher is snappier. The passive listener keeps native scrolling smooth. Every animation below reads this one value and nothing else.

Drive the scene from progress

With progress in hand, the scene becomes a function of scroll. Rotate an object, dolly the camera, fade a material — all keyed to the same number.

const knot = new THREE.Mesh(
  new THREE.TorusKnotGeometry(1, 0.32, 160, 24),
  new THREE.MeshStandardMaterial({ color: 0x7c8bff, roughness: 0.35 })
)
scene.add(knot, new THREE.HemisphereLight(0xbfd0ff, 0x0a0a18, 1.2))
function frame() {
  requestAnimationFrame(frame)
  progress += (target - progress) * 0.08
  knot.rotation.y = progress * Math.PI * 2      // full turn across the page
  camera.position.z = 6 - progress * 3          // dolly in as you scroll
  renderer.render(scene, camera)
}
frame()

Because the scene reads progress and nothing else, you can preview any point by setting progress directly — invaluable during build and QA. Keep any buy action or specifications in HTML, never baked into the canvas, so they stay crawlable and copyable, a discipline shared across interactive websites.

Add GSAP ScrollTrigger for pinning

Manual mapping is enough for a single moving scene. For multi-section storytelling — where a scene pins in place while you scroll through it — GSAP ScrollTrigger is the production-grade tool. It owns the pin, the scrub and the resize-and-refresh lifecycle so you do not reinvent them.

import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
const state = { p: 0 }
gsap.to(state, {
  p: 1, ease: 'none',
  scrollTrigger: { trigger: '#stage', start: 'top top', end: '+=2000', scrub: 1, pin: true }
})
// the render loop reads state.p exactly like progress above

The scrub value adds easing, and pin: true makes a section feel like a chapter rather than a passing element. This is the pattern behind most premium scrollytelling, and it drops cleanly into a reusable 3D component.

Sync HTML captions

Scroll animation usually needs HTML captions to appear in step with 3D beats. Use IntersectionObserver for the DOM side rather than more scroll math — it is cheaper and does not fight the render loop.

const io = new IntersectionObserver((entries) => {
  for (const e of entries) e.target.classList.toggle('-in', e.isIntersecting)
}, { threshold: 0.5 })
document.querySelectorAll('.caption').forEach(el => io.observe(el))

The scene reads progress; the HTML reads visibility. Two simple independent systems are far more maintainable than one giant scroll handler trying to do both.

Performance on mobile

Scroll scenes are GPU-bound. Three levers matter most: cap devicePixelRatio, render on demand, and stop entirely when the canvas is off-screen. On-demand rendering alone often halves battery use on a resting page.

let visible = true
new IntersectionObserver(([e]) => visible = e.isIntersecting).observe(canvas)
function frame() {
  requestAnimationFrame(frame)
  if (!visible || document.hidden) return           // parked
  progress += (target - progress) * 0.08
  if (Math.abs(target - progress) < 1e-4) return     // settled: skip render
  renderer.render(scene, camera)
}

Test on a mid-range Android phone, not a laptop — mobile GPUs are where scroll jank appears first. Uncapped devicePixelRatio and rendering every frame when nothing changed are the two usual culprits, both fixed above.

SEO and accessibility

Scroll animation does not hurt SEO if the semantic layer stays in HTML, and it stays accessible if it honors reduced motion. Keep the heading, copy and links crawlable beside an aria-hidden canvas, and resolve to a final state when motion is reduced.

if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
  progress = 1; renderer.render(scene, camera)       // final state, no animation
}

Search engines index the DOM, not the canvas, so real HTML content ranks while the WebGL enhances it. And because content never hides behind the animation, a reduced-motion or WebGL-less visitor still gets the full page — the baseline for commercial work delivered for agencies.

Manual mapping versus GSAP

Manual scrollY with a lerp is best for one moving scene with minimal dependencies, at the cost of owning pin, resize and refresh yourself. GSAP ScrollTrigger is best for multi-section pinning and scrubbing, at the cost of a library and learning its trigger lifecycle. Match the tool to the number of scenes and the smoothness you actually need — reach for manual mapping first, and add GSAP when you genuinely need pinning across several sections, not because it looks impressive in a tutorial.

How AETumi builds scroll animation

AETumi is an AI-native 3D web platform for production Three.js and WebGL sites, React and Next.js components, and MCP workflows for assistants such as Claude Code. The AETumi standard for scroll animation is opinionated: progress-driven scenes, a crawlable HTML layer, capped devicePixelRatio, on-demand rendering, and a reduced-motion path. Every pattern here ships in real templates, and the same scene can be scaffolded through an MCP workflow so an assistant edits owned source rather than generating a scene from nothing.

Technical proof on GitHub

The AETumiApp/threejs-scroll-animation repository documents the pin, scrub and reveal patterns and includes a runnable example scene built on the progress-timeline model described here. Use the repository as the code proof and this article as the architecture and decision context.

How it works, step by stepAETumi technical diagram — How it works, step by stepNext.js (routing, SSR, image opt)React Three Fiber (declarative 3D)Three.js / WebGL (GPU rendering)GSAP / scroll (motion)
How it works, step by step

FAQ

How do I animate a Three.js scene on scroll? Map scroll position to a 0 to 1 value by dividing scrollY by the maximum scrollable distance, ease it, and drive the camera, objects and materials from that value in the render loop each frame. Never mutate the scene inside a scroll event handler.

Do I need GSAP for Three.js scroll animation? No — you can map scrollY to progress and drive Three.js manually for a single moving scene. GSAP ScrollTrigger is recommended when you need to pin sections and scrub multi-section timelines with reliable resize and refresh handling.

Does Three.js scroll animation hurt SEO? Not if the semantic content stays in HTML and the canvas is decorative. Search engines index the DOM, not the canvas, so keep headings, copy and links crawlable and mark the canvas aria-hidden.

How do I keep scroll animation fast on mobile? Cap devicePixelRatio, render on demand, pause when the canvas is offscreen or the tab hidden, and use passive scroll listeners. Heavy scroll scenes are the most common cause of jank on mobile GPUs.

How do I make scroll animation accessible? Honor prefers-reduced-motion by resolving the scene to its final state instead of animating, keep all content in HTML so nothing hides behind the animation, and never hijack native scroll so the keyboard keeps working.

Continue with the 3D scroll hub, Three.js, React Three Fiber, reusable 3D components, interactive websites, the MCP workflow, and options for agencies.

Conclusion

Scroll-driven 3D animation with Three.js is one clean pipeline: scroll to a normalized progress value, ease it, and drive the scene from that single number, with GSAP ScrollTrigger added when you need pinning. Wrap it in a crawlable, accessible, performance-capped shell and the cinematic part becomes easy to extend and safe to ship. Build the pipeline once, keep the HTML honest, and a scroll-driven Three.js scene becomes a durable asset rather than a fragile demo.

More from the AETumi library

Real, production-ready assets — preview the motion, grab the source.

Browse all 3D website templates →