Back to Blog
Back to Blog
Tutorials·GSAP·April 21, 2026·8 min read

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.

GSAP useGSAP hook in React and Next.js - animation setup with refs and cleanup

Estimated reading time: 9 minutes | Skill level: Intermediate

If you've tried using GSAP in React with
useEffect
, you've probably run into the cleanup problem. Animations that keep running after a component unmounts. ScrollTriggers that don't get destroyed. Memory leaks in development mode that are hard to trace. The official solution is the
useGSAP
hook from
@gsap/react
. It handles cleanup automatically, scopes your selectors, and gives you a clean way to handle event-driven animations.

Here's everything you need to know.

What You'll Need

  • React 18+ or Next.js 14+
  • GSAP installed in your project
  • Basic familiarity with
    useRef
    and
    useEffect

Installation

npm install gsap @gsap/react
The
@gsap/react
package is a thin wrapper maintained by the GreenSock team. It adds the
useGSAP
hook and nothing else. GSAP itself is still the main package.

The 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:

  1. gsap.registerPlugin(useGSAP)
    runs once outside the component, not inside it.
  2. The
    scope: containerRef
    option means
    .hero-title
    and
    .hero-subtitle
    only match elements inside that container.
  3. 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
could match any element with that class. Scoping pins the search to the component's subtree.
// 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 });
For most animations, passing
scope
is the safe default. The only time you skip it is when you're intentionally targeting elements outside the component.

Using 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> ); }
When using direct refs like this, you don't need
scope
because you're not using selector strings at all.

Visual: PLACEHOLDER - Code side-by-side showing selector-based vs ref-based GSAP targeting in React

Animating on State Changes

By default,
useGSAP
runs once after the initial render (like
useEffect
with an empty dependency array). If you need to re-run animations when state changes, pass a dependency array:
const [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 });
The
revertOnUpdate: true
option tells GSAP to clean up the previous animation run before running again. Without it, you can end up with conflicting tweens when
isOpen
flips quickly.

Event-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 the
useGSAP
context. That means it won't be cleaned up on unmount.
const 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 });
contextSafe
wraps the function so it runs inside the GSAP context. If the component unmounts before the animation completes, GSAP handles the cleanup correctly instead of trying to update a detached node.

This 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> ); }
Store the timeline in a ref (
useRef
) so it's accessible in the event handlers without being a dependency. Creating it with
paused: true
prevents autoplay.

With ScrollTrigger

ScrollTrigger works the same way inside
useGSAP
. GSAP handles the cleanup automatically.
import { 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> ); }
When this component unmounts,
useGSAP
automatically calls
ScrollTrigger.kill()
for any triggers created inside the hook. No manual cleanup needed.

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 inside
useGSAP
(or
useEffect
). Never call
gsap.*
or
ScrollTrigger.*
at the top level of a module.
// 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
In Next.js App Router, add
"use client"
to any component file that uses GSAP. Server Components cannot use browser APIs.

Still Using useEffect?

If you can't use
@gsap/react
for some reason,
gsap.context()
inside
useEffect
is the fallback:
import { 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> ); }
The key difference: with
useEffect
you have to manually call
ctx.revert()
in the cleanup. With
useGSAP
, that happens automatically. I'd always recommend
useGSAP
when it's available.

Common Mistakes

Not registering the plugin. Forgetting
gsap.registerPlugin(useGSAP)
means the hook won't behave correctly.

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. Use
contextSafe
or store tweens in refs. Running GSAP code outside the hook. GSAP code at module level or in render functions runs during SSR. Keep all GSAP code inside
useGSAP
or
useEffect
.

Key Takeaways

  • Use
    useGSAP
    from
    @gsap/react
    instead of
    useEffect
    for GSAP in React.
  • Always pass
    scope
    when using selector strings so animations don't leak outside the component.
  • Wrap event-handler animations in
    contextSafe
    to ensure proper cleanup.
  • All GSAP code must run in
    useGSAP
    or
    useEffect
    . Never at the top level in Next.js.
  • ScrollTrigger cleanup is automatic inside
    useGSAP
    .

Take It Further

If you're building scroll animations in React, combine
useGSAP
with the patterns in GSAP ScrollTrigger Examples. The two work seamlessly together. For complex sequences, read the GSAP Timeline Tutorial. Timelines inside
useGSAP
work exactly the same way. All animations in Annnimate are built with
useGSAP
and follow these exact patterns. If you want production-ready React animation code without building from scratch, that's where to start.

Written by

Julian Fella

Julian Fella

Founder

Related reading

GSAP page transitions in Next.js App Router — overlay and reveal animation between routes
Tutorials·April 25, 2026

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.

Read article
Read article
GSAP hover effects tutorial — magnetic buttons, mouse tracking, and card tilt with GSAP
Tutorials·April 24, 2026

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.

Read article
Read article
GSAP stagger animation tutorial — animating lists and grids with timing offset
Tutorials·April 23, 2026

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.

Read article
Read article

On This Page

Stay ahead of the curve

Join 1000+ creators getting animation updates. Unsubscribe anytime.

Animations
Animations
Pricing
Pricing
Blog
Blog
Showcase
Showcase
Changelog
Changelog
Roadmap
Roadmap
XLinkedInInstagram

© 2026 Annnimate · Built by Good Fella

Privacy
Privacy
Terms
Terms
Cookies
Cookies
Refund
Refund