Every viewport has a beat: reveal, hold, transform, release. The page is edited like a trailer.
/scrollTest · cinematic scroll lab
Scroll likea film system.
A no-dependency Next.js route reverse-engineering the design-agency playbook: pinned chapters, parallax depth, scroll-scrubbed filmstrips, mask reveals, foreground/background 3D geometry with trailing motion, native view timelines, and a practical library map.
Why agencies charge for this
It is storyboarding, asset direction, browser math, and QA disguised as vibes.
Transforms, opacity, masks, canvas, and WebGL need a motion budget or the spectacle collapses on mobile.
The expensive part is keeping the story readable with JS disabled, reduced motion, odd screens, and real content.
Fixed foreground and background geometry changes position, rotation, scale, color, and trail lag as the active scene changes.
Pinned storyboard
One sticky viewport, four moving shots.
This section uses a tall scroll container, a sticky 100vh stage, and JavaScript that writes local progress into CSS variables. The cards move because CSS reads --story-shift.
Pin the camera, move the world
The expensive-looking trick is often a sticky section that holds the viewport while every layer scrubs through a storyboarded timeline.
position: sticky + section-local progressSeparate depth layers
Foreground dust, midground type, and background glow all move at different rates so a flat DOM page behaves like a camera move.
parallax CSS variables from requestAnimationFrameReveal with masks, not fades
Clip paths, gradient masks, and drawn SVG paths feel more cinematic than opacity-only entrance animations because they imply physical light.
clip-path, mask-image, stroke-dashoffsetMake the page teach the technique
The best agency pages are not random scroll candy. Each movement demonstrates the product story while hiding the engineering seam.
semantic content + choreographed motion budgetMask reveal + SVG draw
Do not just fade things in. Let the page develop like film.
The diagonal beam below is clipped by --scene-clip. The signal path is drawn by changing stroke-dashoffset. Both are cheap, readable, and art-directable.
Vertical scroll → horizontal reel
A common agency move: scroll down, travel sideways.
Horizontal travel inside vertical scroll
A tall section supplies scroll distance while the inner stage sticks to the viewport and translates a track sideways.
sticky top: 0; transform: translate3d(var(--reel-x), 0, 0)Progress is the timeline
Instead of playing animation on enter, bind the timeline to local scroll progress so users can scrub frame-by-frame.
progress = clamp(-rect.top / (rect.height - viewport))Use light as the transition
Gradient overlays and clip-path reveals make sections feel art-directed instead of componentized.
clip-path: inset(0 calc(100% - var(--scene-clip)) 0 0)The hidden professional detail
Flashy does not mean hostile. Premium builds still respect reduced-motion and keep content readable without choreography.
@media (prefers-reduced-motion: reduce) { transform: none; }Research ledger
The useful toolkit behind flashy scroll pages.
The route itself stays dependency-free. For production client work, these are the patterns and libraries worth reaching for.
Native timelines
Use animation-timeline: scroll() or view() for simple progress bars and reveal effects without measuring scroll in JavaScript.
@supports (animation-timeline: view()) {
.panel {
animation: reveal both linear;
animation-timeline: view();
animation-range: entry 10% cover 40%;
}
}Agency workhorse
For heavy storyboards, ScrollTrigger gives pin, scrub, snap, markers, and timeline composition with less custom geometry code.
gsap.timeline({
scrollTrigger: {
trigger: ".chapter",
pin: true,
scrub: 1,
snap: "labels"
}
});Smooth scroll foundation
Lenis is built for smooth scrolling, WebGL scroll syncing, parallax, anchors, and modern-browser performance.
import Lenis from "lenis";
const lenis = new Lenis({ autoRaf: true });
lenis.on("scroll", ({ progress }) => syncScene(progress));React motion values
useScroll exposes scrollY, scrollYProgress, and element offsets; useTransform maps those values to scale, opacity, y, and clip values.
const { scrollYProgress } = useScroll();
const y = useTransform(scrollYProgress, [0, 1], [0, -240]);
return <motion.div style={{ y }} />;WebGL layer
When the hero needs liquid metal, particles, depth of field, or camera choreography, put WebGL behind semantic DOM copy.
<Canvas>
<Scene scrollProgress={progress} />
</Canvas>
// In useFrame: camera.position.z = 8 - progress * 3No-dependency baseline
A lean production baseline is observer-gated sections plus one rAF scroll loop that writes CSS variables and lets CSS render.
const progress = clamp(
(innerHeight - rect.top) / (innerHeight + rect.height)
);
el.style.setProperty("--scene-progress", progress.toFixed(4));Standing on one foot
Cinematic scroll = sticky scenes + scrubbed progress + depth layers + disciplined fallbacks.
Dimwit take: add parallax. Midwit take: install three animation libraries. Highwit take: storyboard the narrative first, use the browser's native scroll primitives where possible, then pay GSAP/Lenis/WebGL only when the shot demands it.