Back to News & Insights
Artificial Intelligence September 5, 2026 · 9 min read

Your Scroll Animations Look Amateur. Here's the GSAP + Lenis Setup That Fixes It

I've built enough animated portfolio sites and agency landing pages at this point that I can usually...

Your Scroll Animations Look Amateur. Here's the GSAP + Lenis Setup That Fixes It

I've built enough animated portfolio sites and agency landing pages at this point that I can usually tell within the first three seconds of scrolling whether a site was built by someone who actually understands scroll animation, or someone who just copied a GSAP tutorial and called it a day.

I remember the first time I tried to recreate one of those Awwwards style hero sections, the ones where text fades and slides as you scroll and everything feels buttery and expensive. I copied the GSAP code almost exactly from a tutorial. Same triggers, same easing, same everything. On my laptop, using my trackpad, it looked incredible. I was proud of it. Then I opened it on my client's Windows machine with a regular mouse, and it looked like it was having a seizure. Stuttering, jumping, completely different animation than what I built. That was the moment I realized the problem was never really the animation. The problem was what the animation was reading from.

Here's the part nobody explains properly when they show you a GSAP demo. When you scroll a normal webpage, the browser doesn't give you a smooth continuous stream of scroll position. It gives you scroll position in little discrete jumps. How big those jumps are depends on the device, the input method, the browser, even the operating system. A trackpad on a Mac behaves differently than a mouse wheel on Windows, which behaves differently again on a touchscreen.

Now think about what ScrollTrigger is actually doing under the hood. It's constantly reading your scroll position and mapping it to animation progress. If the scroll position itself is jumpy and inconsistent, then no matter how well you write your animation code, the output is going to inherit that same jumpiness. You could have the most perfectly tuned easing curve in the world and it still won't matter, because the input feeding it is unstable.

This is why pinned sections jump instead of smoothly transitioning. This is why parallax looks incredible on your machine and choppy on a client's laptop. This is why sometimes an animation feels like it's racing ahead of your scroll and other times it feels like it's lagging behind. It's almost never a GSAP problem. It's a scroll problem.

Once I understood this, everything clicked. Every site that had that expensive, smooth feeling wasn't doing anything wildly different in their GSAP code. They were animating against a different kind of scroll entirely, a smoothed, virtual scroll layer instead of raw browser scroll. That's where Lenis comes in.

Lenis is a smooth scroll library, but calling it that undersells what it's really doing. What it actually does is intercept the raw scroll input from your mouse, trackpad, or touch, and instead of applying it directly to the page, it interpolates it into a smooth continuous value over time. Then it applies that smoothed value to the actual scroll position.

The result is that your page now scrolls with this nice eased motion instead of the raw jumpy input. But more importantly for us, it also gives GSAP something stable and predictable to read from. Instead of ScrollTrigger reading messy native scroll events, it reads Lenis's smoothed output. Same animation code, completely different feeling result.

This is the exact setup I use now on basically every client build at my agency that needs scroll driven animation. It's not complicated once you understand why each piece exists, but there are a few steps that people skip or get wrong, and those are exactly the steps that make the difference between something that feels amateur and something that feels premium.

Now here's where a lot of tutorials start you off in a way that actually causes problems down the line. They'll show you initializing Lenis with its own separate animation loop, something like this.

This will work, technically. Your page will scroll smoothly. But if you're also running GSAP animations tied to scroll, you now have two separate animation loops running independently, Lenis's own requestAnimationFrame loop, and GSAP's internal ticker. They're not talking to each other. They're each doing their own thing on their own timing. And when two things that are supposed to be in sync are actually running independently, you get drift. Small timing mismatches that build up over time, especially noticeable on longer pinned sections where even a tiny desync becomes visually obvious.

I want to point out that easing function for a second too, because it's not just a random formula I pasted in. That exponential curve gives you a quick initial response and then a soft, gradual settle. If you swap it for a simple linear easing, the scroll will still be smooth technically, but it loses that premium feeling. It'll feel more mechanical, more like a slider than an actual physical scroll. Small detail, but it matters more than people think.

Here's the actual fix for that drift and desync problem, and it's the part I almost never see explained properly in the tutorials that get shared around. Instead of letting Lenis run its own animation frame loop separately from GSAP, you hand control over to GSAP's own internal ticker, and let it drive both Lenis and your animations from the exact same clock.

Notice that I removed the separate requestAnimationFrame loop entirely here. You don't need it anymore, because GSAP's ticker is now the single source of truth driving everything. Lenis updates on GSAP's clock, and ScrollTrigger updates whenever Lenis fires a scroll event. Everything is finally reading from the same timeline instead of three separate clocks trying to loosely agree with each other.

That last line, gsap.ticker.lagSmoothing(0), is one of those things that looks like a minor detail but genuinely changes the feel of scroll linked animation. By default, GSAP has this lag smoothing feature that tries to compensate for dropped frames by essentially fast forwarding animation time to catch up. That's a great feature for something like a button hover animation or a modal transition, where the user won't notice or care about a tiny time skip. But on scroll linked animation, where the animation position is directly tied to scroll position, that kind of catch up jump is very visually obvious. It looks like a stutter or a jump. Turning it off means GSAP just processes frames as they come without trying to be clever about catching up, which is exactly what you want here.

Once that foundation is in place, writing the actual animations is pretty normal GSAP work.

I want to call out that scrub value specifically, because it's another one of those small details that most people either skip or don't think about. You can set scrub to true, which ties the animation progress directly and instantly to scroll position with zero delay. Or you can set it to a number, like scrub: 1, which adds that number of seconds as a kind of catch up delay between where your scroll actually is and where the animation currently is.

Try both side by side on the same animation and you'll immediately feel the difference. Scrub true feels rigid, like the animation is welded directly to your scrollbar. Scrub with a number like 1 gives you this slight lag that actually reads as smoothness to the eye, like the animation is following your scroll rather than being glued to it. It's a strange thing to explain in words, but once you see it in action you'll understand immediately why almost every polished site uses a numeric scrub value instead of true.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation