How to add scroll animations in Next.js
Updated July 18, 2026
The reliable stack is GSAP ScrollTrigger for the scroll-linked animation and Lenis for smooth scrolling, running in a client component on the Next.js App Router. Register ScrollTrigger, create triggers inside a useEffect (or gsap.context for cleanup), and let Lenis drive the scroll while forwarding its position to ScrollTrigger.update.
For a single continuous 'story' โ a camera move, a build, a walk โ drive an all-intra-encoded <video>'s currentTime from scroll progress instead of animating DOM. That's the technique behind the most cinematic scroll sites.
1. Smooth scrolling with Lenis
Native scroll is fine, but scrubbed animation reads far better on a smoothed scroll. Instantiate Lenis once (in a provider), and on every Lenis 'scroll' event call ScrollTrigger.update so GSAP stays in lockstep. Add Lenis to the GSAP ticker rather than its own rAF loop, and disable lag smoothing so scrubbing stays exact.
Set history.scrollRestoration = 'manual' so a restored deep-scroll position doesn't fight Lenis on load, and always respect prefers-reduced-motion by skipping pins and scrubs.
2. Scroll-linked animation with GSAP ScrollTrigger
ScrollTrigger links an animation's progress to scroll position. Use `scrub: true` to tie a timeline directly to the scrollbar, `pin: true` to hold a section while its content animates, and start/end strings (e.g. 'top top' / 'bottom bottom') to define the range. Wrap everything in gsap.context(scope) and revert it on unmount so React re-renders don't leak triggers.
SplitText (free since 2025) handles character- and line-level reveals โ the staggered word rises you see in premium heroes.
3. Scroll-scrubbed video for continuous stories
When the story is one continuous shot, don't animate the DOM โ scrub a video. Map scroll progress to a target time, and on the GSAP ticker lerp video.currentTime toward it. Gate the ticker with `if (!duration || video.readyState < 2) return;` so seeking doesn't abort the download every frame.
Crucially, re-encode the clip all-intra so every frame is a keyframe (`ffmpeg -i in.mp4 -an -c:v libx264 -g 1 -pix_fmt yuv420p -movflags +faststart out.mp4`). Without it the browser snaps to the nearest keyframe and the scrub judders.
4. Common gotchas
Re-measure on resize with invalidateOnRefresh: true, or pinned and horizontal sections mis-calculate after a viewport or font change. Kill triggers on unmount. And if you change a font token with Turbopack, clear .next โ it caches @theme font vars and silently falls back to a system font otherwise.
Frequently asked
Do I need GSAP for scroll animations in Next.js?
Not strictly, but it's the most capable option and free as of 2025. For simple reveals, Framer Motion or the Intersection Observer suffice; for pinned sections, scrubbed timelines and video scrubbing, GSAP ScrollTrigger is the practical choice.
Does Lenis work with the Next.js App Router?
Yes. Initialise it in a 'use client' provider component, add it to the GSAP ticker, and forward its scroll event to ScrollTrigger.update. Set history.scrollRestoration to 'manual' to avoid load-time drift.
Why is my scroll-scrubbed video juddering?
Because it isn't encoded all-intra. Standard H.264 keyframes are seconds apart, so seeking snaps between them. Re-encode with ffmpeg's `-g 1` so every frame is a keyframe and currentTime can land anywhere.