The Lenis ScrollTrigger GSAP Ticker Fix: Why Animations Freeze
Struggling with frozen GSAP ScrollTrigger animations when using Lenis? Learn why it happens and the simple Lenis ScrollTrigger gsap.ticker fix to sync them.
At JRV Systems, we often build websites for Malaysian brands that require a premium, fluid user experience. This frequently involves combining GSAP (GreenSock Animation Platform) for animations with a smooth scrolling library like Lenis. While powerful together, they can cause a frustrating issue: ScrollTrigger animations, especially scrub-based ones, appear to freeze or stutter.
The animation only updates when you stop scrolling, defeating the purpose of a smooth, interactive effect. This happens because of a fundamental conflict in how they handle scroll updates. Fortunately, there is a reliable, four-line Lenis ScrollTrigger gsap.ticker fix that resolves this by properly synchronising their update loops.
The Core Problem: Mismatched Update Loops
To understand the fix, you first need to understand the problem. It boils down to who is in control of the scroll event.
By default, GSAP's ScrollTrigger listens to the browser's native scroll events. It's efficient and works perfectly on its own. However, Lenis is designed to completely take over the scrolling behaviour. It disables the native scroll and creates its own virtual, smoothed scroll value. Lenis updates this value continuously using requestAnimationFrame, which is a browser API for running code efficiently right before the next repaint, typically 60 times per second.
When Lenis is active, the native scroll event that ScrollTrigger is listening for doesn't fire continuously. It might only fire when the Lenis-driven scroll starts or stops. As a result, ScrollTrigger doesn't receive the constant stream of updates it needs to smoothly scrub through an animation timeline. Your animation state is only calculated at the beginning and end of the scroll, making it look frozen in between.
The Solution: Syncing with gsap.ticker
The correct approach is to tell GSAP to get its timing updates directly from Lenis, ensuring they are perfectly in sync. We do this by hooking into GSAP's own internal requestAnimationFrame loop, known as gsap.ticker.
Here is the essential code snippet that forms the Lenis ScrollTrigger gsap.ticker fix:
lenis.on('scroll', ScrollTrigger.update)
This first line is a basic connection. It tells ScrollTrigger to update its internal calculations whenever Lenis emits a 'scroll' event. This helps, but it isn't perfectly smooth on its own because it's still reactive, not proactive.
The magic happens with the next part:
gsap.ticker.add((time)=>{ lenis.raf(time * 1000) })
This is the core of the fix. We are adding a function to GSAP's ticker, which runs on every single frame. Inside this function, we call lenis.raf(), which is Lenis's own function for advancing its scroll simulation. By passing GSAP's time parameter to it (multiplied by 1000 to convert from seconds to milliseconds), we ensure Lenis calculates its new scroll position in perfect lockstep with GSAP's animation updates.
Finally, it's good practice to add:
gsap.ticker.lagSmoothing(0)
This line tells GSAP's ticker to not try and 'catch up' if the browser tab becomes inactive and then active again, which can cause a sudden jump in the animation. It ensures a more stable and predictable experience.
Real-World Implementation Notes
We've used this exact technique on several projects, from interactive landing pages for tech startups in Kuala Lumpur to elegant e-commerce sites for local fashion brands. The goal is always to deliver an experience that feels polished and intentional. A jerky scroll animation immediately breaks that illusion.
When implementing, remember these points:
- Load Order: Make sure both the Lenis and GSAP/ScrollTrigger libraries are loaded before you run this initialisation code.
- Instantiation: Create your new Lenis instance first, then apply the ticker fix.
- Centralised Code: Place this setup code in a central part of your application's JavaScript, where it runs once when the page loads.
Handling Edge Cases: iOS and Accessibility
No solution is perfect without considering edge cases. Two important ones are iOS Safari and user accessibility preferences.
iOS Safari: Older versions of iOS had notoriously tricky momentum scrolling physics that could sometimes conflict with JavaScript-driven scrolling. While the gsap.ticker method is robust, it's always critical to test thoroughly on real iPhones. The smooth scrolling experience should feel natural, not like it's fighting the user's gesture.
Accessibility (prefers-reduced-motion): Some users enable a setting in their operating system to request less motion, often due to vestibular disorders. Forcing a complex, motion-heavy experience on them is poor practice. We should always check for this preference and disable decorative animations and smooth scrolling if requested. You can do this with a simple JavaScript check:
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
If this returns true, you should not initialise Lenis or your more intense ScrollTrigger effects.
How to Confirm the Fix is Working
Don't just trust that the code works; verify it. You can use your browser's built-in developer tools to confirm you're getting a smooth frame rate.
- Open your browser's DevTools (F12 or Ctrl+Shift+I).
- In Chrome, go to the Performance monitor (you may need to find it under 'More tools'). In Firefox, it's the 'Performance' tab.
- Look for the Frame Rate (FPS) chart.
- Start scrolling up and down your page where the ScrollTrigger animation is.
Without the fix, you will likely see a very spiky, low FPS count. With the Lenis ScrollTrigger gsap.ticker fix applied correctly, you should see a stable line near 60 FPS (or whatever your monitor's refresh rate is), indicating a smooth, jank-free animation.