Skip to content

ScrollTrigger timing issue: useGSAP animations start before ScrollTrigger is ready #73

@quentinbrohan

Description

@quentinbrohan

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

  1. Create a component that uses the documented useGSAP pattern from the README (README.md:63-74).
  2. Use ScrollTrigger configuration as shown in documentation.
  3. Place component below the fold or in a page component.
  4. 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

// 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

  1. Manual plugin registration in each component:
import { ScrollTrigger } from "gsap/ScrollTrigger"  
  
useGSAP(() => {  
  gsap.registerPlugin(ScrollTrigger)
  // ... rest of animation code  
})
  1. Re-export useGSAP by adding the content of scroll-trigger.tsx to 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions