The Lenis ScrollTrigger gsap.ticker Fix for Frozen Animations
Struggling with GSAP ScrollTrigger animations freezing when using Lenis? Learn why it happens and the simple gsap.ticker.add fix to sync their update loops.
The Core Problem: Lenis, ScrollTrigger, and Mismatched Tickers
Modern web design often combines GreenSock Animation Platform (GSAP) for powerful animations and Lenis for a smooth, high-quality scrolling feel. The combination is excellent for creating premium user experiences, something we frequently implement for our clients at JRV Systems. However, a common issue arises when using GSAP's ScrollTrigger with scrub: true alongside Lenis: the animation stutters or freezes completely during the scroll.
The root cause is a conflict in how each library handles updates. By default, GSAP's ScrollTrigger listens for the browser's native scroll event to update its animation progress. Lenis, to achieve its smooth effect, intercepts these native events. It then calculates its own smoothed scroll position and applies it, often using CSS transforms. This means the native scroll event doesn't fire continuously as the user scrolls. Instead, it might fire only at the beginning and end of the scroll gesture.
Because ScrollTrigger isn't receiving the constant stream of updates it expects, animations linked to the scrollbar position (scrubbed animations) don't update. They appear frozen until the Lenis scroll comes to a complete stop, creating a jarring experience.
The Solution: The gsap.ticker.add Fix
To solve this, we must bypass ScrollTrigger's reliance on the native scroll event and instead synchronize it directly with Lenis's render cycle. This ensures that for every frame Lenis renders, GSAP also gets a chance to update. The Lenis ScrollTrigger gsap.ticker fix is surprisingly concise and effective.
You need to add a few lines to your JavaScript where you initialize Lenis and GSAP:
// Assumes you have already initialized Lenis, e.g., const lenis = new Lenis()
// This line tells ScrollTrigger to use Lenis's scroll values
lenis.on('scroll', ScrollTrigger.update)
// This is the core fix: it syncs the two libraries' update loops
gsap.ticker.add((time) => {
lenis.raf(time * 1000)
})
// Optional but recommended for responsiveness
gsap.ticker.lagSmoothing(0)
Let's break down what this code does:
lenis.on('scroll', ScrollTrigger.update): This tells ScrollTrigger to perform its position calculations every time Lenis emits ascrollevent. This is a good first step but isn't sufficient on its own to solve the freezing issue.gsap.ticker.add(...): This is the crucial part. We are tapping into GSAP's own internalrequestAnimationFrameloop, known as its "ticker". On every single frame that GSAP updates, we manually call Lenis'sraf(requestAnimationFrame) function. This forces Lenis to render in perfect sync with GSAP. Thetime * 1000conversion is necessary because GSAP's ticker provides time in seconds, while Lenis'srafmethod expects milliseconds.gsap.ticker.lagSmoothing(0): This disables a GSAP feature that tries to compensate for lag. In a synchronized setup like this, it's better to disable it to ensure the animation timing is as direct and responsive as possible.
Verifying the Fix: How to Confirm 60fps Smoothness
After implementing the fix, you should verify that it's working as expected. The best way to do this is with your browser's developer tools.
- Open your browser's DevTools (usually F12 or Cmd+Option+I).
- In Chrome or Edge, find the "Rendering" tab. You may need to click the three-dot menu to find it.
- Check the box for "Frame rate (FPS)". This will overlay an FPS meter on your page.
Now, scroll up and down the page where your scrubbed ScrollTrigger animation is located. Before the fix, you would likely see the FPS counter drop significantly or show erratic numbers during the scroll. After implementing the gsap.ticker.add fix, you should see a stable frame rate, ideally hovering around 60 FPS (or higher, depending on your monitor's refresh rate). This is the objective confirmation that your animation is updating smoothly on every frame.
Edge Cases and Best Practices
While this fix is robust, there are two important considerations for a production-ready website.
iOS Safari Behaviour
Mobile browsers, particularly Safari on iOS, have unique power-saving behaviours that can affect scroll-linked animations. While this gsap.ticker fix generally works well, it is essential to test your implementation on a physical iPhone. Ensure the scrolling feels responsive and doesn't introduce any unexpected stuttering, especially after the page has been idle for a moment.
Accessibility and Reduced Motion Some users enable a "reduced motion" setting in their operating system to prevent motion sickness or distractions. It is a critical accessibility practice to respect this preference. You should wrap your smooth scrolling and complex animation logic in a media query check.
// Only enable smooth scroll and the ticker fix if the user has not requested reduced motion
if (!window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
const lenis = new Lenis()
lenis.on('scroll', ScrollTrigger.update)
gsap.ticker.add((time) => {
lenis.raf(time * 1000)
})
gsap.ticker.lagSmoothing(0)
// Your ScrollTrigger animations would be initialized here
}
At JRV Systems, we make this a standard practice for all client projects. For users who prefer reduced motion, the site gracefully falls back to native browser scrolling, ensuring a comfortable and accessible experience for everyone.
Conclusion: A Simple Fix for a Professional Finish
The conflict between Lenis and GSAP ScrollTrigger stems from their independent update mechanisms. By using gsap.ticker.add to synchronize their render loops, you can resolve the issue of frozen scrub animations. This small, four-line fix is often the difference between a frustrating user experience and the polished, professional feel that modern web applications demand. It's a fundamental technique for anyone building high-end, motion-rich websites.