Jump to content
Search Community

Recommended Posts

Posted

Hi GreenSock team 👋

I'm working with Next.js 15 and trying to orchestrate animations across multiple components using a shared masterTimelinethrough context.

Each component defines its own gsap.timeline() and then adds it to the global timeline via masterTimeline.add(). For example:

I have two components: Boxand Nav

- Box has its tl with 4 tweens and inserts a label after the second tween (addLabel('afterSecondTween')).
- Nav defines its own timeline and tries to insert it at that label inside the master timeline using masterTimeline.add(navTL, 'afterSecondTween').

🎯 My goal is to synchronize timelines between different components, so that they run in sequence or in specific positions.

💣 The result: Nav always animates at the start of the timeline.

I’ve confirmed that:
- The Nav timeline is added with the same label name.
- But it still plays immediately (as if the label had time = 0).

 

💬 My question is:

Is it correct to insert timelines at labels from other components in GSAP + React?

Is this synchronization possible?
What is the correct or recommended way to synchronize animations between different components  in GSAP and React?

 

Is this pattern okay:
1. Each component creates its own timeline
2. One of them defines a label at a specific point
3. Another component inserts its timeline at that label

 

Or... is there a better / more robust way to orchestrate animations across components in React using GSAP?
 

Thanks in advance for your insights 🙏
 

Here’s a minimal CodePen that replicates the problem:  
📎 CodePen

 

See the Pen emYwJWz by cpiocova (@cpiocova) on CodePen.

Posted

Hi,

 

I don't have a lot of time right now to go through all this but you have a logical issue in your code that is causing the problem, right here:

const Box = ({ onReady }) => {
  const boxRef = useRef(null);
  const master = useMasterTimeline();

  useEffect(() => {
    const tl = gsap.timeline();
    tl.to(boxRef.current, { x: 300, duration: 3 }) // tween 1
      .to(boxRef.current, { y: 300, duration: 3, onComplete: () => {
      master.addLabel("afterSecondTween")                
      } }) // tween 2
      .to(boxRef.current, { x: 0, duration: 3 })   // tween 3
      .to(boxRef.current, { y: 0, duration: 3 });  // tween 4

    master.add(tl, 0);
    onReady(); 
  }, []);

  return <div id="box" ref={boxRef}>Box</div>;
};

Basically you're adding the label after the second Tween in that timeline has completed, this happens 6 seconds after the Timeline instance on the Nav component is created, so when you run this code:

useEffect(() => {
  const navTL = gsap.timeline();
  navTL.to(navRef.current, {
    opacity: 1,
    duration: 1,
    onStart: () => console.log("🚀 Nav TL started")
  });

  master.add(navTL, "afterSecondTween");
}, []);

That label doesn't exist because it hasn't been created yet.

 

Also is worth noticing that the Nav component is rendered and executed before the Box component, so there is no way that the label can be added before the Timeline in the Nav component is created, for that you'll have to check the ready state property, which I advice you to add to the React context instance in order to avoid too many parent<->child communication, which can make your app far more complex than it has to be. Just set that property in the same context provider in order to simplify everything, like that you can listen to the ready event in the Nav component and create the Timeline after the Timeline in the Box component is ready and the label actually exists in the master Timeline, something like this:

const Box = ({ onReady }) => {
  const boxRef = useRef(null);
  const master = useMasterTimeline();

  useGSAP(() => {
    const tl = gsap.timeline();
    const getTime = () => {
      master.add("afterSecondTween", tl.duration());
    };
    tl.to(boxRef.current, { x: 300, duration: 3 }) // tween 1
      .to(boxRef.current, { y: 300, duration: 3, }) // tween 2
      .call(getTime())
      .to(boxRef.current, { x: 0, duration: 3 })   // tween 3
      .to(boxRef.current, { y: 0, duration: 3 });  // tween 4

    master.add(tl, 0);
    onReady(); 
  });

  return <div id="box" ref={boxRef}>Box</div>;
};

Finally I strongly advice you to use our useGSAP hook instead of the useEffect/useLayoutEffects hooks, you can learn more about it here:

https://gsap.com/resources/React/

 

Hopefully this helps

Happy Tweening!

Posted

Thanks for your help and guidance on this topic 🙏. After facing many issues trying to sync timelines between different components in React, I managed to develop a modular and reusable solution that works for me and I believe is perfectly suited to projects with complex animations using React. It centralizes all timelines into a masterTimeline, handles labels automatically, and allows you to define dependencies between components declaratively. The solution is already working in my project, and I've published it on GitHub in case it's useful to anyone else: Github

 

I hope this can be useful to someone!!

 

See the Pen ZYEdvLb by cpiocova (@cpiocova) on CodePen.

 

PD: I didn't use useGSAP in the pen because codepen wouldn't let me.

Posted

Hi,

 

Good job on the code, looks great! 🙌

 

The only advice I would give you is regarding a Tween/Timeline that could be added to the master timeline on the provider, that animates a DOM element on a component that is removed from the component tree either by a route change, a conditional rendering setup, a filter or API call. In those cases you could end up with instances in the Timeline that use targets that are not there and create a unexpected delay for other Tweens/Timelines. So perhaps you could warn your users about that particular situation in order to prevent unexpected results.

 

Thanks for creating and sharing this with the community! 💚

Posted

Thank you very much Rodrigo, nice touch, I'll work on it!

Posted

Hey again! 👋

Thanks to your feedback, I made it more robust and flexible. Here's what it supports now:

New two features: (isReadyToPlay - onDependencyFail)

  • isReadyToPlay flag

The master timeline won't start until all component timelines are registered. You can combine this with image or asset loading to delay .play() until everything is ready.

const { isReadyToPlay, masterTimeline } = useMasterTimeline();
useGSAP(() => {
  if (!masterTimeline || !isReadyToPlay || isAssetsLoading) return;
  masterTimeline.play();
}, {dependencies: [masterTimeline, isReadyToPlay, isAssetsLoading]});
  • onDependencyFail fallback

If a timeline depends on a label that never exists (e.g. from a component that wasn’t mounted), you can:

  • Skip or log it
  • Run fallback animations (gsap.to(...))

  • Return an alternate timeline

  • Even register fallback labels for other timelines to depend on

onDependencyFail Examples

Here are 4 valid ways to handle missing dependencies using the onDependencyFail fallback. This helps prevent errors when a timeline depends on a label that was never registered (e.g. due to route changes or conditional rendering).

 

1. Do nothing – just let it warn

registerSyncedTimeline({
  id: 'circle',
  dependsOn: ['nav.label_missing'],
  createTimeline: () => gsap.timeline().to(ref.current, { scale: 1.5 }),
  // onDependencyFail is not defined
})
🟢 If the label is missing, a warning is logged: 
LOG: [TimelineProvider] Skipping timeline "circle" because: Label 'nav.label_missing' not found

 

2. Run a side effect (no timeline)

registerSyncedTimeline({
  id: 'circle',
  dependsOn: ['nav.label_missing', 'hero.logo'],
  createTimeline: () => gsap.timeline().to(ref.current, { scale: 1.5 }),
  onDependencyFail: (_, missingLabels) => {
    // Example: fallback visual state
    if (missingLabels.includes("nav.label_missing")) {
        gsap.to(ref.current, { scale: 1, opacity: 1 })
    }else{
    	gsap.set(ref.current, { xPercent: 100, opacity: 0 })
    }
  },
})
🟢 This won't register any animation in the master timeline, but applies a fallback style.
 

3. Return a fallback GSAP timeline

registerSyncedTimeline({
  id: 'circle',
  dependsOn: ['nav.label_missing'],
  createTimeline: () => gsap.timeline().to(ref.current, { scale: 1.5 }),
  onDependencyFail: () => {
    return gsap.timeline().to(ref.current, {
      backgroundColor: 'orange',
      duration: 1,
    })
  },
})
🟢 The fallback timeline is inserted at the start (time = 0) of the master timeline.
 

4. Return a fallback timeline + fallback labels

const { registerSyncedTimeline, masterTimeline } = useMasterTimeline();
  
const getTL = () => {
    const tl = gsap.timeline();
    tl.to(ref.current, {
      y: -10,
      backgroundColor: "purple",
      duration: 3,
    })
      .addLabel("after-color")
      .to(ref.current, {
        scale: 1.5,
        duration: 1,
      });
    return tl;
  };

  useGSAP(
    () => {
      registerSyncedTimeline({
        id: "circle",
        dependsOn: ["nav.missing_label"],
        createTimeline: getTL,
        labels: {
          circleAfterColor: (tl) => tl.labels["after-color"] ?? tl.duration(),
        },
        onDependencyFail: () => {
          const tl = getTL();
          return {
            timeline: tl,
            labels: {
              circleAfterColor: tl.labels["after-color"] ?? tl.duration(),
            },
            startAt: masterTimeline.labels["home.safe_rendering_label"] ?? 0,
          };
        },
      });
    },
    { scope: ref }
  );
🟢 This registers the fallback animation AND its internal labels, so other components can still depend on circle.circleAfterColor

 

📦I added it to NPM and Github in case anyone is interested!

👉 NPM: react-gsap-master-timeline
👉 GitHub: github.com/cpiocova/react-gsap-master-timeline

 

I hope this helps other developers working with GSAP and React. Thanks again for this amazing community💚

 

 

 

 

  • Like 1

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...