Jump to content
GreenSock

GSAP + React, Advanced Animation Techniques.


| GreenSock
78971

header-2.png

 

Are you working with React and looking to really advance your GSAP animation skills? You're in the right place. This guide contains advanced techniques and some handy tips from expert animators in our community.

This is not a tutorial, so feel free to dip in and out as you learn. Think of it as a collection of recommended techniques and best practices to use in your projects.

Animating with GSAP gives you unprecedented levels of control and flexibility. You can reach for GSAP to animate everything — from simple DOM transitions to SVG, three.js, canvas or WebGL — your imagination is the limit. More importantly, you can rely on us. We obsess about performance, optimizations and browser compatibility so that you can focus on the fun stuff. We've actively maintained and refined our tools for over a decade and there are no plans to stop. Lastly, if you ever get stuck, our friendly forum community is there to help.

Going forward we will assume a comfortable understanding of both GSAP and React.

If you're starting out we highly recommend reading our foundational article first - First Steps & Handy Techniques..

Online Playgrounds

Get started quickly by forking one of these starter templates:

In the last article, we covered creating our first animation, and how to create and control timelines within a React component. But there are times where you may need to share a timeline across multiple components or construct animations from elements that exist in different components.

In order to achieve this, we need a way to communicate between our components.

There are 2 basic approaches to this.

  1. a parent component can send down props, e.g. a timeline
  2. a parent component can pass down a callback for the child to call, which could add animations to a timeline.

Note that we are using useState instead of useRef with the timeline. This is to ensure the timeline will be available when the child renders for the first time.

function Box({ children, timeline, index }) {
  const el = useRef();
  // add 'left 100px' animation to timeline
  useLayoutEffect(() => {    
    timeline && timeline.to(el.current, { x: -100 }, index * 0.1);
  }, [timeline]);
  
  return <div className="box" ref={el}>{children}</div>;
}

function Circle({ children, timeline, index, rotation }) {
  const el = useRef();
  
  useLayoutEffect(() => {   
    // add 'right 100px, rotate 360deg' animation to timeline
    timeline && timeline.to(el.current, {  rotate: rotation, x: 100 }, index * 0.1);
  }, [timeline, rotation]);
  
  return <div className="circle" ref={el}>{children}</div>;
}

function App() {    
  const [tl, setTl] = useState();
     
  return (
    <div className="app">   
      <button onClick={() => setReversed(!reversed)}>Toggle</button>
      <Box timeline={tl} index={0}>Box</Box>
      <Circle timeline={tl} rotation={360} index={1}>Circle</Circle>
    </div>
  );
}

function Box({ children, addAnimation, index }) {
  const el = useRef();
  
  useLayoutEffect(() => {
    const animation = gsap.to(el.current, { x: -100 });
    addAnimation(animation, index);
    
    return () => animation.progress(0).kill();
  }, [addAnimation, index]);
  
  return <div className="box" ref={el}>{children}</div>;
}

function Circle({ children, addAnimation, index, rotation }) {
  const el = useRef();
  
  useLayoutEffect(() => {
    const animation = gsap.to(el.current, { rotate: rotation, x: 100 });
    addAnimation(animation, index);
    
    return () => animation.progress(0).kill();
  }, [addAnimation, index, rotation]);
  
  return <div className="circle" ref={el}>{children}</div>;
}


function App() {
  // define a timeline
  const [tl, setTl] = useState();
  // pass a callback to child elements, this will add animations to the timeline
  const addAnimation = useCallback((animation, index) => {    
    tl.add(animation, index * 0.1);
  }, [tl]);
     
  return (
    <div className="app">   
      <button onClick={() => setReversed(!reversed)}>Toggle</button>
      <Box addAnimation={addAnimation} index={0}>Box</Box>
      <Circle addAnimation={addAnimation} index={1} rotation="360">Circle</Circle>
    </div>
  );
}

Passing down props or callbacks might not be ideal for every situation.

The component you're trying to communicate with may be deeply nested inside other components, or in a completely different tree. For situations like this, you can use React's Context.

Whatever value your Context Provider provides will be available to any child component that uses the useContext hook.

const SelectedContext = createContext();

function Box({ children, id }) {  
  const el = useRef();
  const { selected } = useContext(SelectedContext);
  const ctx = gsap.context(() => {});
  
  useLayoutEffect(() => {
    return () => ctx.revert(); 
  }, []);
  
  useLayoutEffect(() => {
    ctx.add(() => {
      gsap.to(el.current, {
        x: selected === id ? 200 : 0
      });
    });
  }, [selected, id]);
  
  return <div className="box" ref={el}>{children}</div>;
}

function Boxes() {
  return (
    <div className="boxes">
      <Box id="1">Box 1</Box>
      <Box id="2">Box 2</Box>
      <Box id="3">Box 3</Box>
    </div>  
  );  
}

function Menu() {
  
  const { selected, setSelected } = useContext(SelectedContext);
  
  const onChange = (e) => {
    setSelected(e.target.value);
  };
  
  return (
    <div className="menu">      
      <label>
        <input 
          onChange={onChange} 
          checked={selected === "1"}
          type="radio"             
          value="1" 
          name="selcted"/> Box 1
      </label>    
      <label>
        <input 
          onChange={onChange} 
          checked={selected === "2"}
          type="radio"             
          value="2" 
          name="selcted"/> Box 2
      </label>  
      <label>
        <input 
          onChange={onChange} 
          checked={selected === "3"}
          type="radio"             
          value="3" 
          name="selcted"/> Box 3
      </label>  
    </div>    
  );
}

function App() {    
  const [selected, setSelected] = useState("2");
     
  return (
    <div className="app">   
      <SelectedContext.Provider value={{ selected, setSelected }}>    
        <Menu />
        <Boxes />
      </SelectedContext.Provider>
    </div>
  );
}

 

Passing around props or using Context works well in most situations, but using those mechanisms cause re-renders, which could hurt performance if you're constantly changing a value, like something based on the mouse position.

To bypass React’s rendering phase, we can use the useImperativeHandle hook, and create an API for our component.

const Circle = forwardRef((props, ref) => {
  const el = useRef();
    
  useImperativeHandle(ref, () => {           
    
    // return our API
    return {
      moveTo(x, y) {
        gsap.to(el.current, { x, y });
      }
    };
  }, []);
  
  return <div className="circle" ref={el}></div>;
});

Whatever value the imperative hook returns will be forwarded as a ref

function App() {    
  const circleRef = useRef();
       
  useLayoutEffect(() => {    
    // doesn't trigger a render!
    circleRef.current.moveTo(300, 100);
  }, []);
    
  return (
    <div className="app">   
      <Circle ref={circleRef} />
    </div>
  );
}

 

Creating reusable animations is a great way to keep your code clean while reducing your app’s file size. The simplest way to do this would be to call a function to create an animation.

function fadeIn(target, vars) {
  return gsap.from(target, { opacity: 0, ...vars });
}

function App() {    
  const box = useRef();
    
  useLayoutEffect(() => {
    const animation = fadeIn(box.current, { x: 100 });
  }, []);
  
  return <div className="box" ref={box}>Hello</div>;
}

For a more declarative approach, you can create a component to handle the animation.

function FadeIn({ children, vars }) {
  const el = useRef();
  
  useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      animation.current = gsap.from(el.current.children, { 
        opacity: 0,
        ...vars
      });
    });
    return () => ctx.revert();       
  }, []);
  
  return <span ref={el}>{children}</span>;
}
  
function App() {      
  return (
    <FadeIn vars={{ x: 100 }}>
      <div className="box">Box</div>
    </FadeIn>
  );
}

If you want to use a React Fragment or animate a function component, you should pass in a ref for the target(s).

GSAP provides a way to create reusable animations with registerEffect()

function GsapEffect({ children, targetRef, effect, vars }) {  
  
  useLayoutEffect(() => {
    if (gsap.effects[effect]) {
      ctx.add(() => {
        animation.current = gsap.effects[effect](targetRef.current, vars);
      });
    }
  }, [effect]);
    
  return <>{children}</>;
}

function App() {      
  const box = useRef();
  
  return (
    <GsapEffect targetRef={box} effect="spin">
      <Box ref={box}>Hello</Box>
    </GsapEffect>
  );
}

To animate elements that are exiting the DOM, we need to delay when React removes the element. We can do this by changing the component’s state after the animation has completed.

function App() {      
  const boxRef = useRef();
  const [active, setActive] = useState(true);  
  const [ctx, setCtx] = useState(gsap.context(() => {}, app));
  
  useLayoutEffect(() => {
    ctx.add("remove", () => {
      gsap.to(ctx.selector(".box"), {
        opacity: 0,
        onComplete: () => setActive(false)
      });
    });
    return () => ctx.revert();
  }, []);
  
  return (
    <div>
      <button onClick={ctx.remove}>Remove</button>
      { active ? <div ref={boxRef}>Box</div> : null }
    </div>
  );
}

The same approach can be used when rendering elements from an array.

function App() {    
  
  const [items, setItems] = useState([
    { id: 0 },
    { id: 1 },
    { id: 2 }
  ]);
  
  const removeItem = (value) => {
    setItems(prev => prev.filter(item => item !== value));
  }
  
  useLayoutEffect(() => {
    ctx.add("remove", (item, target) => {
      gsap.to(target, {
        opacity: 0,
        onComplete: () => removeItem(item)
      });
    });
    return () => ctx.revert();
  }, []);
  
  return (
    <div>
      {items.map((item) => (
        <div key={item.id} onClick={(e) => ctx.remove(item, e.currentTarget)}>
          Click Me
        </div>
      ))}
    </div>
  );
}

However - you may have noticed the layout shift - this is typical of exit animations. The Flip plugin can be used to smooth this out.

In this demo, we’re tapping into Flip’s onEnter and onLeave to define our animations. To trigger onLeave, we have to set display: none on the elements we want to animate out.

If you find yourself reusing the same logic over and over again, there’s a good chance you can extract that logic into a custom hook. Building your own Hooks lets you extract component logic into reusable functions.

Let's take another look at registerEffect() with a custom hook

function useGsapEffect(target, effect, vars) {
  const [animation, setAnimation] = useState();
  
  useLayoutEffect(() => {
    setAnimation(gsap.effects[effect](target.current, vars));    
  }, [effect]);
  
  return animation;
}

function App() {      
  const box = useRef();
  const animation = useGsapEffect(box, "spin");
  
  return <Box ref={box}>Hello</Box>;
}

Here are some custom hooks we've written that we think you may find useful:

Memoises a GSAP Context instance.

function useGsapContext(scope) {
  const ctx = useMemo(() => gsap.context(() => {}, scope), [scope]);
  return ctx;
}

Usage:

function App() {
  const ctx = useGsapContext(ref);
  
  useLayoutEffect(() => {
    ctx.add(() => {
      gsap.to(".box", {
        x: 200,
        stagger: 0.1
      });
    });
    return () => ctx.revert();
  }, []);
  
  return (
    <div className="app" ref={ref}>
      <div className="box">Box 1</div>
      <div className="box">Box 2</div>
      <div className="box">Box 3</div>
    </div>
  );
}

See demo on codepen

 

This hook helps solve the problem of accessing stale values in your callbacks. It works exactly like useState, but returns a third value, a ref with the current state.

function useStateRef(defaultValue) {
  const [state, setState] = useState(defaultValue);
  const ref = useRef(state);

  const dispatch = useCallback((value) => {
    ref.current = typeof value === "function" ? value(ref.current) : value;
    setState(ref.current);
  }, []);

  return [state, dispatch, ref];
}

Usage:

const [count, setCount, countRef] = useStateRef(5);
const [gsapCount, setGsapCount] = useState(0);  

useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    gsap.to(".box", {
      x: 200,
      repeat: -1,
      onRepeat: () => setGsapCount(countRef.current)
    });
  }, app);
  return () => ctx.revert();
}, []);

see demo on codepen

You might see a warning if you use server-side rendering (SSR) with useLayoutEffect. You can get around this by conditionally using useEffect during server rendering. This hook will return useLayoutEffect when the code is running in the browser, and useEffect on the server.

caveat: Any "from" state that doesn't match the server-side rendered HTML/CSS content will still suffer from a flash of unstyled content while the JavaScript is being parsed, run and hydrated.

read more about useLayoutEffect and server rendering

const useIsomorphicLayoutEffect = typeof window !== "undefined" 
  ? useLayoutEffect 
  : useEffect;

Usage:

function App() {    
  const app = useRef();
    
  useIsomorphicLayoutEffect(() => {
    const ctx = gsap.context(() => {
      gsap.from(".box", { opacity: 0 });
    }, app);
    return () => ctx.revert();
  }, []);
  
  return (
    <div className="app" ref={app}>
      <div className="box">Box 1</div>
    </div>
  );
}

see demo on codepen


If there is anything you'd like to see included in this article, or if you have any feedback, please leave a comment below so that we can smooth out the learning curve for future animators.

Good luck with your React projects and happy tweening!

 

  • Like 8
  • Thanks 3

Get an all-access pass to premium plugins, offers, and more!

Join the Club

We love seeing what you build with GSAP. Don't forget to let us know when you launch something cool.

- Team GreenSock



User Feedback

Recommended Comments

I think the last section of `useIsomorphicLayoutEffect` only avoids the warning but does not actually fix the problem, because the rendering and hydration process of a server side rendered or static generated page would go like this:

  1. The HTML/CSS content is downloaded and displayed with normal opacity
  2. The JS is then downloaded and parsed, and React would kick in and start the hydration, which would run the layout effect to set the opacity to 0
  3. And then lastly the animation of opacity from 0 to 1 would run, but only after the content already showed and blinked

In conclusion, `useLayoutEffect` would only avoid the "blink" for subsequent rendering when React is hydrated, but not when the content is from the rendered/generated HTML before the React hydration.

 

(Sometimes people could think this works because the whole process happens very quickly, but in my opinion it's a real problem.)

  • Like 1
Link to comment
Share on other sites

34 minutes ago, React said:

I think the last section of `useIsomorphicLayoutEffect` only avoids the warning but does not actually fix the problem

 

Thanks for the input, @React. You're absolutely correct. For fading in, it would probably be better to just have the opacity set to 0 ahead of time. But we needed a demo, and it's easy to demonstrate fading in. Other uses for useLayoutEffect, like when pinning ScrollTriggers aren't as easy to demonstrate in a simple CodePen.

 

Do you think it be better if we just removed that part from the guide?

  • Like 2
Link to comment
Share on other sites

21 minutes ago, OSUblake said:

Do you think it be better if we just removed that part from the guide?

 

I don't think we'd need to remove the part, but maybe mention the caveat that any "from" state that doesn't match the server side rendered HTML/CSS content will still have the blink problem during JS being downloaded, parsed and run and React hydration.

 

However, I do think this is a pretty common case rather than an edge case.

  • Like 2
Link to comment
Share on other sites

I was wondering if anyone could help with the example above Creating Reusable Animations.

It states  "If you want to use React Fragment or .... pass the a ref to the targets "

I am new to React. Could anyone be kind and show me how that passing is done

 

 

 

Link to comment
Share on other sites

Hi @MikeGajdos

 

Here's a more in depth article on creating reusable animation components...

Creating Reusable Animation Components with React and GSAP | by Nathan Sebhastian | Bits and Pieces (bitsrc.io)

 

The part about fragment is for advanced usage, and I wouldn't worry about that if you're new to React. The fragment is only needed if you don't want your animation component to create a wrapper element. Instead you would wrap the children with a fragment, like this...

 

return (
  <>{children}</>  
);

 

But it's gets more complicated than that because you need to get a ref the children. Here are some demos that didn't make the final cut of the article, but show how to get refs for children inside a fragment. 

 

Using a custom ref function...

 

https://codepen.io/GreenSock/pen/XWRqrjY

 

This demo also uses a custom ref function, but the bounds are passed in via a ref.

 

https://codepen.io/GreenSock/pen/qBmYEjx

 

Again, if you're new to React, I would focus more on article I linked to. The demos I just posted might better suited someone who plans on making a library for other people to use.

 

  • Like 1
Link to comment
Share on other sites

Note that we are using useState instead of useRef with the timeline. This is to ensure the timeline will be available when the child renders for the first time.

 

Why is this necessary? Why could it be not available if you don’t create it as a state?
 

The timeline is just an object right? 


 

Link to comment
Share on other sites

Hi @nickraps

 

You can create a timeline with useState, but it would be better to use a function to initialize it.

// will only create 1 timeline
const [tl, setTl] = useState(() => gsap.timeline());

// creates a new timeline on every render and throws it away
// except for the one created on the first render
const [tl, setTl] = useState(gsap.timeline());

 

Another reason to use useRef with an effect is that you will need to wait for it render when using ScrollTrigger.

 

const [tl, setTl] = useState(() => gsap.timeline({
  scrollTrigger: {
    trigger: ref.current // undefined
  }
}));

 

Link to comment
Share on other sites

@nickraps

 

I probably misunderstood your question, but the issue is still that React will create a new timeline on every single render if you don't use a hook.

 

This shows why you should never create a timeline outside of a hook. The timeline in the reversed effect is referencing a completely different timeline.

 

Storing a timeline in a ref. (codepen.io)

 

 

Link to comment
Share on other sites

@OSUblake

 

Thanks for the explanation! 

I see your point. I agree if you create the timeline in the component function, it should be always in a hook like useState or useRef to prevent creating new ones.

 

However, what if you just create the timeline as a plain variable in the same file as the component, but not in the actual component function? That way, a new timeline also won't be created even if the component re-renders. I guess in this particular case it also works.

Link to comment
Share on other sites

Any tips how to type provided custom hook useArrayRef?

 

Example: const [refItems, setRefItems] = useArrayRef();

 

Adding refs:

 <div ref={setRefItems} .../>

 <div ref={setRefItems} .../>

and finally

 [...refItems.current].forEach(function (index) { ... })

gives:

Type error: Property 'current' does not exist on type 'MutableRefObject<any> | ((ref: any) => any)'. Property 'current' does not exist on type '(ref: any) => any'.


Thanks!

Link to comment
Share on other sites

Hi @Decode

 

I'm not too familiar with React's types, but this seems like it might work.

 

function useArrayRef(): [MutableRefObject<any[]>, (ref: any) => void] {
  const refs = useRef<any[]>([]);
  refs.current = [];
  return [refs, (ref: any) => ref && refs.current.push(ref)];
}

 

  • Thanks 1
Link to comment
Share on other sites



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

×