GSAP in React: How to Use the useGSAP Hook (2026)
Learn how to use GSAP in React with the useGSAP hook. Covers setup, refs, scoping, contextSafe callbacks, ScrollTrigger cleanup, and Next.js SSR patterns.

Estimated reading time: 9 minutes | Skill level: Intermediate
If you've tried using GSAP in React withuseEffectuseGSAP@gsap/reactHere's everything you need to know.
What You'll Need
- React 18+ or Next.js 14+
- GSAP installed in your project
- Basic familiarity with and
useRefuseEffect
Installation
npm install gsap @gsap/react
@gsap/reactuseGSAPThe Basic Pattern
Here's how most GSAP animations work in React:
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
// Register the plugin once, outside the component
gsap.registerPlugin(useGSAP);
export default function HeroSection() {
const containerRef = useRef(null);
useGSAP(() => {
gsap.from(".hero-title", {
y: 40,
opacity: 0,
duration: 0.8,
ease: "power3.out"
});
gsap.from(".hero-subtitle", {
y: 20,
opacity: 0,
duration: 0.6,
ease: "power3.out",
delay: 0.2
});
}, { scope: containerRef });
return (
<div ref={containerRef}>
<h1 className="hero-title">Build Better Animations</h1>
<p className="hero-subtitle">With GSAP and React.</p>
</div>
);
}
Three things to notice:
- runs once outside the component, not inside it.
gsap.registerPlugin(useGSAP) - The option means
scope: containerRefand.hero-titleonly match elements inside that container..hero-subtitle - Cleanup is automatic. When the component unmounts, all animations in this context are reverted.
Why Scope Matters
Without a scope, GSAP's selector strings search the entire document. In a React app with multiple components,.hero-title// Without scope — risky
useGSAP(() => {
gsap.to(".card", { scale: 1.05 }); // matches ALL .card elements on the page
});
// With scope — safe
useGSAP(() => {
gsap.to(".card", { scale: 1.05 }); // only matches .card inside containerRef
}, { scope: containerRef });
scopeUsing Refs Directly
For single elements, targeting by ref is more explicit and avoids string selectors entirely:
export default function AnimatedCard() {
const cardRef = useRef(null);
const titleRef = useRef(null);
useGSAP(() => {
const tl = gsap.timeline();
tl.from(cardRef.current, {
y: 30,
opacity: 0,
duration: 0.7,
ease: "power3.out"
});
tl.from(titleRef.current, {
y: 15,
opacity: 0,
duration: 0.5
}, "-=0.3");
});
return (
<div ref={cardRef} className="card">
<h2 ref={titleRef}>Card Title</h2>
</div>
);
}
scopeVisual: PLACEHOLDER - Code side-by-side showing selector-based vs ref-based GSAP targeting in React
Animating on State Changes
By default,useGSAPuseEffectconst [isOpen, setIsOpen] = useState(false);
const panelRef = useRef(null);
useGSAP(() => {
if (isOpen) {
gsap.to(panelRef.current, { height: "auto", opacity: 1, duration: 0.4 });
} else {
gsap.to(panelRef.current, { height: 0, opacity: 0, duration: 0.3 });
}
}, {
scope: panelRef,
dependencies: [isOpen],
revertOnUpdate: true // revert previous animation before re-running
});
revertOnUpdate: trueisOpenEvent-Driven Animations with contextSafe
Here's a pattern that trips up a lot of developers. If you create a GSAP animation inside an event handler, it runs outside theuseGSAPconst containerRef = useRef(null);
const buttonRef = useRef(null);
useGSAP((context, contextSafe) => {
// This runs at setup time — safely inside context
gsap.from(containerRef.current, { opacity: 0, duration: 0.5 });
// Wrap event handler animations in contextSafe
const handleClick = contextSafe(() => {
gsap.to(buttonRef.current, {
scale: 0.95,
duration: 0.1,
yoyo: true,
repeat: 1
});
});
buttonRef.current.addEventListener("click", handleClick);
// Remove listener in the cleanup return
return () => {
buttonRef.current.removeEventListener("click", handleClick);
};
}, { scope: containerRef });
contextSafeThis matters more than it seems in React apps with frequent unmounts (route changes, conditional rendering, Suspense).
Hover Animations
Hover effects in React often need both a "play" and a "reverse" state. Here's a clean pattern:
export default function AnimatedButton() {
const buttonRef = useRef(null);
const tl = useRef(null);
useGSAP(() => {
tl.current = gsap.timeline({ paused: true });
tl.current.to(buttonRef.current, { scale: 1.04, duration: 0.3, ease: "power2.out" });
}, { scope: buttonRef });
return (
<button
ref={buttonRef}
onMouseEnter={() => tl.current.play()}
onMouseLeave={() => tl.current.reverse()}
>
Hover Me
</button>
);
}
useRefpaused: trueWith ScrollTrigger
ScrollTrigger works the same way insideuseGSAPimport { useRef } from "react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(ScrollTrigger, useGSAP);
export default function AnimatedSection() {
const sectionRef = useRef(null);
useGSAP(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: sectionRef.current,
start: "top 80%",
once: true
}
});
tl.from(".section-heading", { y: 30, opacity: 0, duration: 0.8 });
tl.from(".section-body", { y: 20, opacity: 0, duration: 0.6 }, "-=0.3");
}, { scope: sectionRef });
return (
<section ref={sectionRef}>
<h2 className="section-heading">Section Title</h2>
<p className="section-body">Section content here.</p>
</section>
);
}
useGSAPScrollTrigger.kill()Next.js and Server-Side Rendering
GSAP runs in the browser. In Next.js with the App Router, components can render on the server. The rule is simple: all GSAP code must run insideuseGSAPuseEffectgsap.*ScrollTrigger.*// Wrong — runs during SSR
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger); // This runs on the server. It will fail.
// Right — register inside the component or a client-only effect
"use client";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(ScrollTrigger, useGSAP); // This is fine at module level in 'use client' files
"use client"Still Using useEffect?
If you can't use@gsap/reactgsap.context()useEffectimport { useEffect, useRef } from "react";
import gsap from "gsap";
export default function FallbackComponent() {
const containerRef = useRef(null);
useEffect(() => {
const ctx = gsap.context(() => {
gsap.from(".box", { x: -50, opacity: 0, duration: 0.6 });
}, containerRef);
return () => ctx.revert(); // ALWAYS revert in cleanup
}, []);
return (
<div ref={containerRef}>
<div className="box">Animated</div>
</div>
);
}
useEffectctx.revert()useGSAPuseGSAPCommon Mistakes
Not registering the plugin. Forgettinggsap.registerPlugin(useGSAP)Skipping scope. Without scope, class selectors match the entire document. In an app with multiple instances of the same component, this is a guaranteed bug.
Creating event-handler animations without contextSafe. These won't be cleaned up on unmount. UsecontextSafeuseGSAPuseEffectKey Takeaways
- Use from
useGSAPinstead of@gsap/reactfor GSAP in React.useEffect - Always pass when using selector strings so animations don't leak outside the component.
scope - Wrap event-handler animations in to ensure proper cleanup.
contextSafe - All GSAP code must run in or
useGSAP. Never at the top level in Next.js.useEffect - ScrollTrigger cleanup is automatic inside .
useGSAP
Take It Further
If you're building scroll animations in React, combineuseGSAPuseGSAPuseGSAPWritten by
Julian Fella
Founder
Related reading

GSAP Page Transitions in Next.js: A Practical Guide (2026)
Learn how to build smooth GSAP page transitions in Next.js App Router. Covers overlay animations, exit animations, useGSAP cleanup, and common pitfalls.

GSAP Hover Effects: 5 Patterns Worth Knowing (2026)
Learn how to build GSAP hover effects that feel polished. Covers play/reverse pattern, magnetic effects, quickTo for mouse tracking, and React implementation.

GSAP Stagger: Animate Lists and Grids with Rhythm (2026)
Learn how to use GSAP stagger to animate multiple elements with perfect timing. Covers basic stagger, advanced object syntax, from options, and real-world grid reveals.