Fixing Frozen GSAP ScrollTrigger with Lenis: The gsap.ticker Patch
Your GSAP ScrollTrigger animations freezing with Lenis? The issue is a requestAnimationFrame conflict. Learn the simple, 4-line Lenis ScrollTrigger gsap.ticker fix.
When building modern, fluid websites, a common stack is GSAP for animation, ScrollTrigger for scroll-based interactions, and Lenis for smooth scrolling. It's a powerful combination that can create premium user experiences. However, many developers hit a frustrating wall: scrubbed ScrollTrigger animations freeze during the scroll, only catching up when the movement stops. It looks broken and defeats the purpose of the smooth scroll effect.
This isn't a bug in the traditional sense. It's a logical conflict in how these libraries handle rendering frames. Fortunately, the solution is straightforward and involves synchronizing their update loops. This article explains the problem and provides the definitive Lenis ScrollTrigger gsap.ticker fix.
The Core Problem: A Battle for requestAnimationFrame
To understand the fix, we first need to understand why it breaks. The conflict arises from two different systems trying to control the page's render loop.
-
How GSAP ScrollTrigger Works (Natively): By default, ScrollTrigger listens to the browser's native
scrollevent. When you physically scroll with your mouse wheel or trackpad, this event fires repeatedly, and ScrollTrigger updates the animation's progress accordingly. It's simple and efficient. -
How Lenis Works: Lenis's entire purpose is to override the native scroll behavior. It captures your scroll input, prevents the default browser scroll, and then creates its own silky-smooth scroll animation using
requestAnimationFrame. This is a JavaScript-driven loop that updates the page's scroll position frame by frame, creating the easing effect we want.
The conflict is now clear: Lenis has taken over. The native scroll event that ScrollTrigger is waiting for doesn't fire continuously anymore. It might fire once at the very end of the Lenis scroll, which is why you see the animation suddenly jump to its final state. During the smooth scroll itself, ScrollTrigger is blind; it receives no updates and the animation remains frozen.
The Solution: Synchronizing Lenis and GSAP with gsap.ticker
The fix is to stop relying on the native scroll event and instead hook both libraries into a single, shared update loop. GSAP has its own highly optimized requestAnimationFrame loop called gsap.ticker. We can use this as the single source of truth for time and rendering updates.
Here is the complete code snippet you need after initializing Lenis and GSAP:
const lenis = new Lenis()
lenis.on('scroll', ScrollTrigger.update)
gsap.ticker.add((time)=>{
lenis.raf(time * 1000)
})
gsap.ticker.lagSmoothing(0)
Let's break down these four crucial lines:
lenis.on('scroll', ScrollTrigger.update): This line tells ScrollTrigger to run itsupdate()method every time Lenis emits ascrollevent. This ensures that ScrollTrigger's positional calculations (start/end points) are always correct based on Lenis's virtual scroll position.gsap.ticker.add((time)=>{...}): This is the core of the Lenis ScrollTrigger gsap.ticker fix. We are adding a new function to GSAP's main ticker, which runs on every single frame. Inside this function, we calllenis.raf().lenis.raf(time * 1000): This command tells Lenis to advance its smooth scroll animation for the current frame. Now, instead of running its own loop, Lenis is being driven directly by GSAP. They are perfectly synchronized. We multiplytimeby 1000 because GSAP's ticker provides time in seconds, while Lenis'srafmethod expects milliseconds.gsap.ticker.lagSmoothing(0): This is a performance optimization. It tells GSAP's ticker not to try and compensate for potential lag, which can sometimes cause slight jumps when used with an external driver like this. It results in a more direct and stable connection.
Verifying the Fix: How to Know It's Working
Once you've implemented the code, the first test is visual. Your scrubbed animations should now move smoothly in sync with the Lenis scroll. To be more rigorous, you can use your browser's developer tools.
-
FPS Meter: Open your browser's rendering tools (in Chrome, Command+Shift+P -> "Show rendering" -> "FPS meter"). As you scroll, you should see a stable frame rate, ideally close to your monitor's refresh rate (e.g., 60fps). If the animation is frozen, the frame rate might be high, but the visual update won't happen.
-
Console Logging: Add a temporary
onUpdatecallback to one of your scrubbed GSAP tweens to log its progress.gsap.to('.my-element', { scrollTrigger: { scrub: true, // ... }, x: 500, onUpdate: self => console.log(self.progress.toFixed(3)) });If the fix is working, your console will be flooded with progress values (0.001, 0.002, etc.) as you scroll. If it's broken, you will see nothing, and then a single log of
1.000when the scroll ends.
Important Considerations: iOS Safari and Accessibility
While this fix is robust, there are two important edge cases to consider for production-ready websites.
iOS Safari: Mobile Safari has its own unique scroll physics and aggressive battery-saving behaviors. Even with this fix, you may sometimes encounter jitter on complex scenes, especially during fast flings. There is no magic bullet here. At JRV Systems, our process for projects targeting Malaysian users—where iPhone market share is significant—always includes rigorous testing on physical iPhones and iPads, not just emulators.
Accessibility (prefers-reduced-motion): Smooth scrolling can cause motion sickness or discomfort for some users. It is a web accessibility best practice to respect the prefers-reduced-motion media query. You should wrap your Lenis initialization and the ticker logic in a check for this setting.
- Check if the user prefers reduced motion.
- If they do, simply don't initialize Lenis or the
gsap.tickerconnection. - The site will then fall back to the native browser scroll, which is the desired accessible experience.
Here is how you might implement that gate:
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReducedMotion) {
const lenis = new Lenis()
lenis.on('scroll', ScrollTrigger.update)
gsap.ticker.add((time)=>{
lenis.raf(time * 1000)
})
gsap.ticker.lagSmoothing(0)
}
Conclusion: A Simple Fix for a Professional Stack
The freezing behavior between Lenis and GSAP ScrollTrigger is a classic example of how two excellent libraries can conflict when they both try to manage the same resource. By using gsap.ticker as the master clock, you create a synchronized system where animations are perfectly tied to the smooth scroll, frame by frame.
Implementing this Lenis ScrollTrigger gsap.ticker fix is a small but critical step. It's the kind of technical detail that separates a frustrating user experience from the polished, professional feel that clients expect from modern web applications, whether it's a dynamic e-commerce site or a custom SaaS dashboard. It's a standard part of our front-end development workflow at JRV Systems.