Jump to content
GreenSock

Getting Started with GSAP + React.


| GreenSock
171851

header.png

React and GSAP can be a powerful combination, as evidenced by many of the sites in our showcase. GSAP is a framework-agnostic animation library, you can write the same GSAP code in React, Vue, Angular or whichever framework you chose, the core principles won't change. There are some React-specific tips and techniques that will make your life easier though, let's take a look...

This is not a tutorial, so feel free to dip in and out as you learn. 

Why GSAP?

GSAP is so popular because it delivers unprecedented control and flexibility, and that's exactly what award-winning developers want. You can reach for GSAP to animate anything — from simple DOM transitions to SVG, Three.js, canvas or WebGL, even generic JavaScript objects — your imagination is the limit. Most 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. If you get stuck, our active and friendly forum community is there to help.

A basic understanding of GSAP and React is assumed.

Get started quickly by forking one of these starter templates:

Create a new React App

If you prefer to work locally, Create React App provides a comfortable setup for experimenting with React and GSAP.

To create a project, run:

npx create-react-app gsap-app
cd gsap-app
npm start

Once the project is set up we can install GSAP through npm,

npm i gsap
npm start

Then import it into our app.

import React from "react";
import { gsap } from "gsap";
   
export default function App() {
 return (
   <div className="app">
     <div className="box">Hello</div>
   </div>
 );
}

More detailed information about getting started with React

Additional GSAP installation documentation

Let's start with a common challenge - animating on a user interaction. This is pretty straightforward with React. We can hook into callbacks to fire off animations on certain events like click or hover. In this demo the box is scaling up onMouseEnter, and down onMouseLeave.

But what if we want an animation to fire after the component mounts, without a user triggered callback?

The useLayoutEffect() hook runs immediately AFTER React has performed all DOM mutations. It's a very handy hook for animation because it ensures that your elements are rendered and ready to be animated. Here's the general structure:

const comp = useRef(); // create a ref for the root level element (we'll use it later)

useLayoutEffect(() => {
  
  // -- ANIMATION CODE HERE --
  
  return () => { 
    // cleanup code (optional)
  }
  
}, []); // <- empty dependency Array so it doesn't re-run on every render!

 Don't forget that empty dependency Array! If you omit that, React will re-run the useLayoutEffect() on every render.

In order to animate, we need to tell GSAP which elements we want to target. The React way to access DOM nodes is by using Refs. Refs are a safe, reliable reference to a particular DOM node.

const boxRef = useRef();

useLayoutEffect(() => {
  // Refs allow you to access DOM nodes
  console.log(boxRef) // { current: div.box }
  // then we can animate them like so...
  gsap.to(boxRef.current, {
    rotation: "+=360"
  });
});

return (
  <div className="App">
    <div className="box" ref={boxRef}>Hello</div>
  </div>
);

However - animation often involves targeting many DOM elements. If we wanted to stagger 10 different elements we'd have to create a Ref for each DOM node. This can quickly get repetitive and messy.

So how can we leverage the flexibility of selector text with the security of Refs? Enter gsap.context().

gsap.context() provides two incredibly useful features for React developers, the option of using scoped selectors and more critically - animation cleanup

GSAP Context is different than React Context.

We can pass a Ref into context to specify a scope. All selector text (like ".my-class") used in GSAP-related code inside that context will be scoped accordingly, meaning it'll only select descendants of the Ref. No need to create a Ref for every element!

Here's the structure:

const comp = useRef(); // create a ref for the root level element (for scoping)
const circle = useRef();

useLayoutEffect(() => {
  
  // create our context. This function is invoked immediately and all GSAP animations and ScrollTriggers created during the execution of this function get recorded so we can revert() them later (cleanup)
  let ctx = gsap.context(() => {
    
    // Our animations can use selector text like ".box" 
    // this will only select '.box' elements that are children of the component
    gsap.to(".box", {...});
    // or we can use refs
    gsap.to(circle.current, { rotation: 360 });
    
  }, comp); // <- IMPORTANT! Scopes selector text
  
  return () => ctx.revert(); // cleanup
  
}, []); // <- empty dependency Array so it doesn't re-run on every render
  
// ...

In this example, React will first render the box and circle elements to the DOM, then GSAP will rotate them 360deg. When this component un-mounts, the animations are cleaned up using ctx.revert().

🧠 deep dive...

Refs or scoped selectors?

show more...

Targeting elements by using selector text like ".my-class" in your GSAP-related code is much easier than creating a ref for each and every element that you want to animate - that’s why we typically recommend using scoped selectors in a gsap.context().

An important exception to note is if you’re going to be nesting components and want to prevent against your selectors grabbing elements in child components.

In this example we've got two elements animating in the main App. A box targeted with a scoped class selector, and a circle targeted with a Ref. We've also nested another component inside our app. This nested element also has child with a class name of '.box'. You can see that the nested box element is also being targeted by the animation in the App's effect, whereas the nested circle, which was targeted with a Ref isn't inheriting the animation.

useLayoutEffect() provides us with a cleanup function that we can use to kill animations. Proper animation cleanup is crucial to avoid unexpected behaviour with React 18's strict mode. This pattern follows React's best practices.

gsap.context makes cleanup nice and simple, all GSAP animations and ScrollTriggers created within the function get collected up so that you can easily revert() ALL of them at once.

We can also use this cleanup function to kill anything else that could cause a memory leak, like an event listener.

useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    const animation1 = gsap.to(".box1", { rotation: "+=360" });

    const animation2 = gsap.to(".box2", {
      scrollTrigger: {
        //...
      }
    });
  }, el);

  const onMove = () => {
    //...
  };
  window.addEventListener("pointermove", onMove);

  // cleanup function will be called when component is removed
  return () => {
    ctx.revert(); // animation cleanup!!

    window.removeEventListener("pointermove", onMove); // Remove the event listener
  };
}, []);

 

Within a component based system, you may need more granular control over the elements you're targeting. You can pass props down to children to adjust class names or data atrributes and target specific elements.

React advises to use classes purely for styling and data attributes to target elements for JS functionality like animations. In this article we'll be using classes as they're more commonly understood.

Up until now we've just used refs to store references to DOM elements, but they're not just for elements. Refs exist outside of the render loop - so they can be used to store any value that you would like to persist for the life of a component. 

In order to avoid creating a new timeline on every render, it's important to create the timeline inside an effect and store it in a ref.

function App() {
  const el = useRef();
  const tl = useRef();

  useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      tl.current = gsap
        .timeline()
        .to(".box", {
          rotate: 360
        })
        .to(".circle", {
          x: 100
        });
    }, el);
  }, []);

  return (
    <div className="app" ref={el}>
      <Box>Box</Box>
      <Circle>Circle</Circle>
    </div>
  );
}

This will also allow us to access the timeline in a different useEffect() and toggle the timeline direction.

If we don't pass a dependency Array to useLayoutEffect(), it is invoked after the first render and after every update. So every time our component’s state changes, it will cause a re-render, which will run our effect again. Typically that's wasteful and can create conflicts.

We can control when useLayoutEffect should run by passing in an Array of dependencies. To only run once after the first render, we pass in an empty Array, like []. You can read more about reactive dependencies here.

// only runs after first render
useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    gsap.to(".box-1", { rotation: "+=360" });
  }, el);
}, []);

// runs after first render and every time `someProp` changes
useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    gsap.to(".box-2", { rotation: "+=360" });
  }, el);
}, [someProp]);

// runs after every render
useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    gsap.to(".box-3", { rotation: "+=360" });
  }, el);
});

 

Now that we know how to control when an effect fires, we can use this pattern to respond to changes in our component. This is especially useful when passing down props.

function Box({ children, endX }) {
  const boxRef = useRef();

  // run when `endX` changes
  useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      gsap.to(boxRef.current, {
        x: endX
      });
    });
    return () => ctx.revert();
  }, [endX]);

  return (
    <div className="box" ref={boxRef}>
      {children}
    </div>
  );
}

 

 

We hope this article was helpful - If you have any feedback please leave us a comment below so we can smooth out the learning curve for future animators!

  • Like 14
  • Thanks 1

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

Join the Club

Have you exercised your animation superpowers today? Go make us proud and tell us about it.

- Team GreenSock



User Feedback

Recommended Comments

This updated article is amazing! I had no clue I should have used useRef for the master timeline!!! Solved many of my annoying questions!!! <333

  • Like 2
Link to comment
Share on other sites

This is absolutely mindblowing, I am so happy that I don't need react-transition-group anymore :)))

I got a question though, won't using .from method cause initial render glitch or flash? for example the whole dom might be visible before .from method sets some elements to opacity 0

also May you please add examples of transition between routes? For example when I go from homepage to about page, how to animate that?

Thanks for the amazing article though, I really enjoyed it ❤️ 

  • Like 1
Link to comment
Share on other sites

11 hours ago, Alixsep said:

I got a question though, won't using .from method cause initial render glitch or flash? for example the whole dom might be visible before .from method sets some elements to opacity 0

 

That's we recommended using useLayoutEffect as it will run before the DOM gets painted to the screen. The only time that won't happen is when using SSR and your app hasn't been hydrated. For that, you might need to do something similar to option 2 here.

https://gist.github.com/gaearon/e7d97cdf38a2907924ea12e4ebdf3c85

 

11 hours ago, Alixsep said:

also May you please add examples of transition between routes? For example when I go from homepage to about page, how to animate that?

 

Yeah, we'll work on a demo for that, although it's a little more complicated because it involves keeping track of elements using state and React's Children API.

 

  • Like 1
Link to comment
Share on other sites

When I try to use gsap.utils.selector(), this error pops up "React Hook useEffect has a missing dependency: 'q'. Either include it or remove the dependency array  react-hooks/exhaustive-deps" How do I fix this?

Screenshot (14).png

Link to comment
Share on other sites

That warning is from eslint and won't cause any problems. If you don't want to see the warning, add the eslint-disable-next-line comment above the array, or declare q inside your use effect.

 

useEffect(() => {
	const q = gsap.utils.selector(...);
}, []);

 

Also, using logo1 as the selector context isn't going to work as it has no children. You should use a ref on the root element in your component. Please look at the examples provide above.

 

Link to comment
Share on other sites

Everything working like a charm. Just not 100% on whether an animation.kill() kills the scrolltrigger or do I need to kill the animation & then the scrolltrigger?
 

  useEffect(() => {
    gsap.registerPlugin(ScrollTrigger)
    const animation = gsap.fromTo(
      ref.current,
      {
        opacity: 0
      },
      {
        scrollTrigger: {
          trigger: ref.current,
          start: "top center",
          end: "bottom center",
          scrub: true,
          toggleActions: "restart none none none"
        },
        opacity: 1,
        ease: "power3.inOut"
      }
    )
    return () => {
      animation.kill() // <--- does this kill the scrolltrigger?
      animation.scrollTrigger.kill() // <--- or do I need to kill here too?
    }
  }, [])

 

Link to comment
Share on other sites

If the scrollTrigger is created inside the animation (just like you've done) - Killing the timeline will kill the associated scrollTrigger too.

  • Like 1
Link to comment
Share on other sites

8 hours ago, Cassie said:

If the scrollTrigger is created inside the animation (just like you've done) - Killing the timeline will kill the associated scrollTrigger too.


Thanks. Yeah thought so, just wasn't sure why it was documented to kill the ScrollTriggers specifically. I understand that they create fresh triggers on a re-render but I thought it might just be better to kill the whole thing!

Link to comment
Share on other sites

Is there a typo in the section about avoiding FOUC?  

 

Quote

In order to avoid the flash, we can replace useEffect with useLayoutEffect. useLayoutEffect functions exactly the same as useEffect, but React doesn’t run it until the DOM has been painted.

 

If I understand the above correctly, useLayoutEffect will not run until after the DOM has been painted, which would make it the exact same as useEffect, right?  Isn't useLayoutEffect run before the DOM has been painted?

Link to comment
Share on other sites

Hi @ConnorS

 

I think the bold part was meant to describe the behavior of useEffect, but I can see how that is confusing. I just changed it to this. Is that better?

 

Quote

In order to avoid the flash, we can replace useEffect with useLayoutEffect. useLayoutEffect functions exactly the same as useEffect, but runs before the DOM has been painted.

 

Link to comment
Share on other sites

On 10/20/2021 at 3:27 AM, OSUblake said:

That warning is from eslint and won't cause any problems. If you don't want to see the warning, add the eslint-disable-next-line comment above the array, or declare q inside your use effect.

First, I'd like to say I enjoyed the article! 

However, as an experienced React developer, I frequently come across developers coming to React from other libraries/frameworks. Many of these seem to struggle understanding the dependency array. Some like to use it as a means to write imperative code, declaring triggers in the dependency array - while omitting actual dependencies.

In this case, declaring q inside useEffect is the way to go, or make sure qhas a stable reference and include it in the dependency array. This particular eslint rule comes from the React team, and I think in general we should play by their rules. They know React now, and probably also what's coming.

  • Like 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

×