-
-
Notifications
You must be signed in to change notification settings - Fork 80
Description
TL;DR: Race condition between the Scroll Trigger plugin registration and code execution with useGSAP hook. I added workarounds, but they’re not optimal.
Problem
When using useGSAP with ScrollTrigger in page components, there's a race condition where animations attempt to execute before ScrollTrigger is fully initialized and synced with Lenis. This causes ScrollTrigger-dependent animations to fail silently.
Steps to Reproduce
- Create a component that uses the documented
useGSAPpattern from the README (README.md:63-74). - Use
ScrollTriggerconfiguration as shown in documentation. - Place component below the fold or in a page component.
- Observe console logs showing animation starts before "scrollTrigger loaded".
Expected Behavior
ScrollTrigger should be ready immediately when useGSAP executes, as implied by the documentation.
Actual Behavior
Console logs show timing issue:
animate start: 11:39:15.430Z
scrollTrigger loaded: 11:39:15.566Z // 136ms delay
The <GSAPRuntime /> component loads ScrollTrigger asynchronously (scroll-trigger.tsx:8-14), but page components execute immediately, creating a race condition.
Code Example
- Issue repo: https://github.com/quentinbrohan/satus-usegsap-scrolltrigger-registration-timing-issue
- CodeSandBox (not able to make it run without
bunon the VM) : https://codesandbox.io/p/github/quentinbrohan/satus-usegsap-scrolltrigger-registration-timing-issue/
// From README - should work but doesn't due to timing, just adapted to `fromTo`.
useGSAP(() => {
gsap.fromTo('.target', {
opacity: 0.5,
}, {
onStart: () => {
console.log(new Date().toISOString(), 'useGSAP gsap.To start')
},
y: 100,
opacity: 1,
scrollTrigger: {
trigger: '.target',
start: 'top center',
end: 'bottom center',
scrub: true,
},
})
})Current Workaround
- Manual plugin registration in each component:
import { ScrollTrigger } from "gsap/ScrollTrigger"
useGSAP(() => {
gsap.registerPlugin(ScrollTrigger)
// ... rest of animation code
})- Re-export
useGSAPby adding the content ofscroll-trigger.tsxto have the plugin ready + Lenis sync
import { useGSAP as originalUseGSAP } from "@gsap/react";
import gsap from "gsap";
import { ScrollTrigger as GSAPScrollTrigger } from "gsap/ScrollTrigger";
import { useLenis } from "lenis/react";
import { useEffect } from "react";
export function useGSAP(
callback: Parameters<typeof originalUseGSAP>[0],
options?: Parameters<typeof originalUseGSAP>[1]
): ReturnType<typeof originalUseGSAP> {
// same setup as in scroll-trigger.tsx
const lenis = useLenis(GSAPScrollTrigger.update);
// biome-ignore lint/correctness/useExhaustiveDependencies: no time to type
useEffect(() => {
GSAPScrollTrigger.refresh();
}, [lenis]);
// same config as in components/gsap/scroll-trigger
return originalUseGSAP(() => {
if (typeof window !== "undefined") {
gsap.registerPlugin(GSAPScrollTrigger);
GSAPScrollTrigger.clearScrollMemory("manual");
GSAPScrollTrigger.defaults({
markers: process.env.NODE_ENV === "development",
});
}
// @ts-ignore
callback();
}, options);
}Notes
I tried to put <GSAPRuntime /> higher in the tree in the layout.tsx but facing the same issue.
Environment
- Satūs template (latest)
- Macbook M1, using latest Chrome version (same on Firefox).
- Default GSAPRuntime configuration