Scroll-driven sites are commissioned far more often than they are built well. The failure modes are consistent: the page fights the reader for control of scrolling, the phone battery drains, or the content is trapped inside a canvas where no search engine will ever read it.
None of that is inherent to the technique. It comes from choosing spectacle over structure.
Three techniques, and when each is right
- Scroll-scrubbed video. A short clip whose playhead is tied to scroll position. Scrolling down advances it, scrolling up reverses it. Best for a cinematic moment you want the reader to control.
- Sticky-pinned sections. A tall wrapper with a sticky child. The child stays fixed while the wrapper scrolls past, giving you a progress value from 0 to 1 to drive anything. This is the workhorse.
- CSS 3D depth. Perspective,
translateZand layered parallax. Much cheaper than WebGL, GPU-composited, and sufficient for most "3D" briefs.
Real WebGL (Three.js and similar) is worth it only when you genuinely need arbitrary geometry, lighting or a configurable 3D model. For depth and motion, CSS is lighter, more reliable and far easier to make accessible.
Sticky pinning, without a library
The core pattern is a few lines of CSS. A tall outer element, an inner element that is sticky and exactly one viewport high. Scroll progress through the outer element becomes your animation timeline.
.scene {
height: 300vh; /* the scroll distance you want */
position: relative;
}
.scene__stage {
position: sticky;
top: 0;
height: 100vh;
overflow: hidden;
}
Then compute progress in JavaScript and use it to drive whatever you are animating:
function progressOf(scene) {
const rect = scene.getBoundingClientRect();
const total = rect.height - window.innerHeight;
return Math.min(1, Math.max(0, -rect.top / total));
}
Scroll-scrubbed video: the details that matter
Scrubbing a video is easy to demonstrate and easy to get wrong on real devices. Four things decide whether it feels smooth.
- Encode with short keyframe intervals. Seeking jumps to the nearest keyframe. A normal web encode places one every two seconds, so scrubbing snaps between them. Re-encode with
-g 4or lower. The file grows; the experience becomes usable. - Serve a size per device. A 4K master is unusable on a phone. Ship 1080p, 720p and 480p variants and choose by viewport and connection.
- Set
muted,playsinlineandpreload="auto". Withoutplaysinline, iOS takes the video fullscreen. - Ease the playhead. Do not assign
currentTimedirectly from scroll. Interpolate toward the target in arequestAnimationFrameloop, and the motion becomes smooth instead of stepping.
let target = 0, current = 0;
function tick() {
current += (target - current) * 0.12; // ease toward target
if (Math.abs(target - current) > 0.001) {
video.currentTime = current * video.duration;
}
requestAnimationFrame(tick);
}
The performance budget
Animation is cheap only if you animate the right properties. Anything that forces the browser to recalculate layout on every frame will not hold 60fps on a mid-range phone.
- Animate
transformandopacityonly. Both are composited on the GPU. - Never animate
top,left,width,heightormargin— each triggers layout. - Read layout values once per frame, then write. Interleaving reads and writes causes forced synchronous layout.
- Use
IntersectionObserverto switch off work for off-screen sections. - Use
will-changesparingly and remove it afterwards; every promoted layer costs memory. - Lazy-load heavy assets. A cinematic clip three sections down should not block the first paint.
Accessibility is not optional here
- Honour
prefers-reduced-motion. Some people experience genuine nausea from parallax and scroll-jacking. Provide a static version with the same content. - Never hijack the scroll. Let the native scroll work. Sticky pinning does not take control away; smooth-scroll libraries often do.
- Keep text as text. Content inside a canvas cannot be selected, searched, translated or read by a screen reader.
- Keyboard users must reach everything. Test by tabbing through the whole page.
- Provide a skip link past a long animated sequence.
@media (prefers-reduced-motion: reduce) {
.scene { height: auto; }
.scene__stage { position: static; height: auto; }
* { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
Keeping it indexable
A scroll site ranks exactly like any other site, provided the content is real HTML in the initial response. Problems arise when text is injected by JavaScript after a scroll event that a crawler never fires.
- Every heading and paragraph should exist in the HTML source before any script runs.
- Animation should reveal content that is already present, not create it.
- Check with "view source", not developer tools — the latter shows the page after scripting.
- Provide normal internal links; a crawler cannot scroll to a section that only exists at 70% progress.
Questions this raises
Often not. Sticky positioning plus a small amount of vanilla JavaScript handles most scroll-driven layouts, with no dependency and a much smaller payload. Reach for a library when you need complex timeline orchestration or genuine 3D geometry.
Only if built carelessly. Reserve space for media to protect CLS, lazy-load anything below the fold to protect LCP, and keep per-frame work small to protect INP.
One to three seconds of footage stretched across two to three viewport heights of scroll. Longer clips need proportionally more scroll distance or the motion becomes too fast to follow.
Still stuck after reading this? That is usually the point at which it is worth asking someone. Describe your project or ask on WhatsApp.