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.
- 🤷🏼♀️ If you're just getting going with React, this tutorial from the React team is a great place to start.
- 🤔 Need a GSAP refresher? Take a break and read the getting started guide which covers tweens and timelines.
- 🥳 Feeling confident? Skip straight to part 2 - GSAP + React, advanced animation techniques.
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.
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
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
.
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!
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.
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()
.
🧠 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.
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 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. In this article we'll be using classes as they're more commonly understood.
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.
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 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); });
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> ); }
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!

-
14
-
1
Recommended Comments
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 accountSign in
Already have an account? Sign in here.
Sign In Now