Skip to main content

React

Before we begin

If you're just getting going with React, this tutorial from the React team is a great place to start.

If you're new to GSAP, read our getting started guide.

In need of some extra React help? Hit up the Reactiflux community for expert advice.

Why choose GSAP?

There are React-specific libraries that offer a simpler more declarative approach. So why choose GSAP?

Animating imperatively gives you a lot more power, control and flexibility. Your imagination is the limit. You can reach for GSAP to animate everything from simple DOM transitions to SVG, three.js, canvas or WebGL.

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 listen to the needs of the community.

Lastly, if you ever get stuck, our friendly forum community is there to help. 💚

Going forward we will assume a basic knowledge of both GSAP and React.

Online playgrounds

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.

  1. To create a project, run:

    bash
    npx create-react-app gsap-app
    cd gsap-app
    npm start
  2. Once the project is set up we can install GSAP through npm,

    bash
    npm install gsap
    npm start
  3. 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>
    );
    }

Animating on interaction

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.

loading...

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

Triggering animation on mount - useLayoutEffect()

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!
caution

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

Targeting elements with Refs

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() is your best friend!

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

caution

GSAP Context is different than React Context.

Scoped Selectors

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().

loading...

deep dive...
Refs or scoped selectors?

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.

loading...

Cleaning Up

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
};
}, []);
MatchMedia & Context

gsap.matchMedia() uses gsap.context() under the hood, so if you're already using matchMedia for responsive animation - you can just call revert() on your matchMedia instance instead for cleanup (no need to combine them).

Reusing components

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.

info

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

loading...

Creating and controlling timelines

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.

loading...

Controlling when React creates our animation.

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 whenuseLayoutEffect 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);
});

loading...

Reacting to changes in state

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>
);
}

loading...

info

Are you looking to really advance your GSAP animation skills? The next guide contains advanced techniques and some handy tips from expert animators in our community.