React
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:
React
view React demosReact
view React demosReact
view React demos
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:
bashnpx create-react-app gsap-app
cd gsap-app
npm startOnce the project is set up we can install GSAP through npm,
bashnpm install gsap
npm startThen 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!
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.
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...
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
};
}, []);
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.
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...
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.