The Lenis ScrollTrigger gsap.ticker Fix for Frozen Animations
Animations frozen when using Lenis with GSAP ScrollTrigger? Learn the common cause and the simple 4-line gsap.ticker.add fix to sync their update loops.
The Problem: Why Lenis and ScrollTrigger Don't Always Cooperate
If you build modern websites, you've likely combined GSAP (GreenSock Animation Platform) for animations and a smooth-scrolling library like Lenis. They are powerful tools. GSAP's ScrollTrigger plugin is excellent for creating scroll-driven animations, but when you introduce Lenis, you might see a frustrating issue: scrubbed animations freeze.
The animation plays correctly on page load, but as soon as you scroll, it gets stuck on its first frame. Non-scrubbed, trigger-based animations might still work, but anything tied directly to the scrollbar position fails. This happens because both Lenis and GSAP want to control the requestAnimationFrame loop — the browser's mechanism for running smooth animations.
Lenis takes over the native scroll behaviour to create its smooth effect. In doing so, it effectively hijacks the main update loop. GSAP's internal ticker, which ScrollTrigger relies on to update animations frame-by-frame during a scroll, no longer receives the updates it needs. It's like a clock that has stopped ticking; the animation doesn't know that time (or the scroll position) has advanced.
This is a common hurdle, and fortunately, there's a standard and reliable Lenis ScrollTrigger gsap.ticker fix that synchronises them correctly.
The Solution: A Simple Lenis ScrollTrigger gsap.ticker Fix
To get Lenis and GSAP's ScrollTrigger working together, you need to tell GSAP to get its timing updates from Lenis's animation loop. Instead of two separate loops running out of sync, you create one unified loop controlled by Lenis.
The fix involves adding a listener to Lenis's 'scroll' event and manually updating ScrollTrigger, then connecting GSAP's main ticker to the Lenis requestAnimationFrame function. Here is the essential code snippet:
const lenis = new Lenis()
lenis.on('scroll', ScrollTrigger.update)
gsap.ticker.add((time)=>{
lenis.raf(time * 1000)
})
gsap.ticker.lagSmoothing(0)
Let's break this down:
const lenis = new Lenis(): We initialise Lenis as usual.lenis.on('scroll', ScrollTrigger.update): This tells ScrollTrigger to update its position calculations every time Lenis reports a scroll event. This is crucial for trigger-based (non-scrubbed) animations.gsap.ticker.add((time)=>{...}): This is the core of the fix. We are hooking into GSAP's own ticker.lenis.raf(time * 1000): Inside the GSAP ticker, we call Lenis'sraf(requestAnimationFrame) function on every tick. Thetimeargument from GSAP is in seconds, while Lenis expects milliseconds, so we multiply by 1000. This makes Lenis the driver of the animation frame updates.gsap.ticker.lagSmoothing(0): This optional but recommended line helps prevent jumps in the animation if the browser tab becomes inactive and then active again.
By adding this code after you initialise Lenis and GSAP, you ensure both systems are perfectly synchronised.
Verifying the Fix: Is It Running at 60fps?
How do you know the fix is working correctly? A broken implementation might still seem to work but could be inefficient or jittery. You can confirm that your gsap.ticker is running smoothly by adding a simple console.log.
Modify the ticker function temporarily:
gsap.ticker.add((time, deltaTime) => {
lenis.raf(time * 1000)
console.log(deltaTime)
})
Open your browser's developer console and scroll the page. You should see a steady stream of numbers being logged. deltaTime represents the time in milliseconds since the last frame. On a standard 60Hz monitor, a smooth 60 frames per second (fps) will produce values around 16.67 (1000ms / 60fps). If you see this, your animation loop is healthy.
If the numbers are erratic or much higher, it might indicate performance issues on your page that are unrelated to this specific fix, but it confirms the ticker itself is running.
Important Considerations: iOS Safari and Reduced Motion
While the Lenis ScrollTrigger gsap.ticker fix works across most modern browsers, there are a couple of best practices to keep in mind, especially for accessibility and mobile performance.
iOS Safari: On some versions of iOS, aggressive smooth scrolling can interfere with the browser's native momentum and touch behaviour, leading to a "sticky" or unresponsive feel. It's important to test thoroughly on real devices. Sometimes, reducing Lenis's lerp (linear interpolation) value or disabling it on touch devices is a necessary compromise.
Accessibility: Not everyone wants or can tolerate motion-heavy websites. It's critical to respect the prefers-reduced-motion media query. You can wrap your Lenis initialisation in a check to disable it for users who have this setting enabled.
Here's how you might implement it:
- Check if the user prefers reduced motion.
- If they do, don't initialise Lenis at all. Your site will revert to native browser scrolling.
- If they don't, proceed with the Lenis and GSAP ticker setup.
This ensures a comfortable and accessible experience for all users, which is a core principle in the projects we handle at JRV Systems.
Our Experience Building in Malaysia
We first encountered this exact issue while developing a highly interactive e-commerce site for a client in Kuala Lumpur. The design called for product images that would scale and rotate based on scroll position—a classic use case for GSAP ScrollTrigger with a scrub value. When we added Lenis for a more premium feel, the animations immediately froze.
Implementing the gsap.ticker.add solution resolved the problem cleanly. It's a small piece of code, but it addresses a fundamental conflict between how these two popular libraries operate. Understanding this interaction is key to building the fluid, reliable web experiences that Malaysian businesses expect today.