Access the AI Animation Generator and premium effects
Upgrade your plan to keep generating animations and unlock premium effects.
Master modern React integration patterns for GSAP animations. Learn the useGSAP hook, proper cleanup, refs, server components, and production-ready patterns for React and Next.js.
To use GSAP with React, you need to install both the core GSAP library and the official React package. The @gsap/react package provides the useGSAP hook, which handles all the complexity of integrating GSAP with React's lifecycle and Strict Mode.
npm install gsap @gsap/react
# For Next.js projects (same command)
# The @gsap/react package works with both React and Next.js
After installation, you can import GSAP and the useGSAP hook in your React components. The @gsap/react package is a lightweight wrapper around GSAP that handles React-specific concerns like cleanup and Strict Mode.
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
// Register plugins before using them
gsap.registerPlugin(ScrollTrigger);
Always register GSAP plugins globally in your app or component. You only need to register each plugin once, typically at the top of your component file or in a main setup file. Registering plugins multiple times has no negative effects but is unnecessary.
The useGSAP hook is the recommended way to use GSAP in React. It automatically handles cleanup, works properly with React 18 Strict Mode, and manages animation lifecycle. Without this hook, you'd need to manually kill animations when components unmount to prevent memory leaks.
The hook takes a callback function where you define your animations, and it ensures they're cleaned up when the component unmounts. This is much better than using raw useEffect because it understands GSAP's requirements.
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useRef } from 'react';
export default function FadeInComponent() {
const boxRef = useRef(null);
useGSAP(() => {
// Animation code runs here
gsap.from(boxRef.current, {
opacity: 0,
y: 50,
duration: 1,
ease: 'power2.out'
});
});
return (
<div ref={boxRef} style={{ width: '100px', height: '100px', backgroundColor: '#00FF66' }}>
Box
</div>
);
}
useEffect(() => {
gsap.to(el, { x: 100, duration: 1 });
// No cleanup! Animation
// references leak in memory
}, []);
useGSAP(() => {
gsap.to(el, { x: 100, duration: 1 });
// Cleanup is handled automatically!
});
The useGSAP hook automatically kills all animations when your component unmounts. You don't have to remember to call gsap.killTweensOf() or manage cleanup manually. This is especially important in Single Page Applications where components mount and unmount frequently.
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useRef, useState } from 'react';
export default function AnimatedButton() {
const buttonRef = useRef(null);
const [isAnimating, setIsAnimating] = useState(false);
// Re-run animation when isAnimating changes
useGSAP(() => {
if (isAnimating) {
gsap.to(buttonRef.current, {
scale: 1.1,
duration: 0.3,
ease: 'power2.out',
onComplete: () => setIsAnimating(false)
});
}
}, [isAnimating]); // Dependency array like useEffect
return (
<button
ref={buttonRef}
onClick={() => setIsAnimating(true)}
style={{ padding: '10px 20px', fontSize: '16px' }}
>
Click me!
</button>
);
}
useGSAP accepts a dependency array just like useEffect. When dependencies change, the animation callback re-runs. This is how you re-trigger animations based on state changes. Always include dependencies if your animation relies on props or state.
GSAP needs to target DOM elements. In React, you use useRef to create references to DOM elements. The useGSAP hook's scope parameter allows you to target child elements using CSS selectors within that scope, making it safer and more predictable in React components.
There are two main approaches: using a ref to target a single element, or using the scope parameter to query child elements within a container. The scope approach is particularly powerful because it keeps your animations isolated to a specific component.
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useRef } from 'react';
export default function CardAnimation() {
const cardRef = useRef(null);
const imageRef = useRef(null);
useGSAP(() => {
// Animate the image inside the card
gsap.to(imageRef.current, {
scale: 1.05,
duration: 0.6,
ease: 'power2.out'
});
});
return (
<div ref={cardRef} className="card">
<img ref={imageRef} src="image.jpg" />
<h3>Product</h3>
</div>
);
}
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useRef } from 'react';
export default function ListAnimation() {
const containerRef = useRef(null);
useGSAP(() => {
// Query all .list-item elements within containerRef
// This is scoped—only animates items in THIS component
gsap.from('.list-item', {
opacity: 0,
x: -50,
duration: 0.6,
stagger: 0.1,
ease: 'power2.out'
});
}, { scope: containerRef }); // Scope parameter!
return (
<ul ref={containerRef}>
<li className="list-item">Item 1</li>
<li className="list-item">Item 2</li>
<li className="list-item">Item 3</li>
</ul>
);
}
The scope parameter is crucial in React because it keeps animations isolated to your component. Without scope, a CSS selector like '.list-item' would target elements throughout your entire page, potentially animating elements from other components. Always use scope when targeting child elements with selectors.
Best practice: Use refs for single elements, use scope + selectors for multiple child elements. This keeps your animations predictable and prevents conflicts when multiple instances of a component exist on the same page.
When you create event handlers (click, hover, etc.) that trigger animations, you need to use contextSafe. This method wraps your animation code so it's properly cleaned up when the component unmounts, preventing "memory leak" warnings in React Strict Mode.
contextSafe is a method returned from useGSAP that wraps functions to ensure they respect the component's lifecycle. It's essential when animations are triggered dynamically by events rather than immediately on mount.
const handleClick = () => {
gsap.to(box, { x: 100 });
// Causes warnings in Strict Mode
}
<button onClick={handleClick}>
const { contextSafe } = useGSAP();
const handleClick = contextSafe(() => {
gsap.to(box, { x: 100 });
// Properly cleaned up!
});
<button onClick={handleClick}>
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useRef } from 'react';
export default function InteractiveBox() {
const boxRef = useRef(null);
const { contextSafe } = useGSAP();
// Wrap event handlers with contextSafe
const handleMouseEnter = contextSafe(() => {
gsap.to(boxRef.current, {
scale: 1.1,
backgroundColor: '#00FF66',
duration: 0.3,
ease: 'power2.out'
});
});
const handleMouseLeave = contextSafe(() => {
gsap.to(boxRef.current, {
scale: 1,
backgroundColor: '#00CC88',
duration: 0.3,
ease: 'power2.out'
});
});
return (
<div
ref={boxRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{
width: '100px',
height: '100px',
backgroundColor: '#00CC88',
cursor: 'pointer'
}}
>
Hover me!
</div>
);
}
Destructure contextSafe from useGSAP and wrap any function that creates animations. This is required for click handlers, hover handlers, scroll listeners, and any other event-driven animations. It ensures cleanup happens properly when the component unmounts.
ScrollTrigger is GSAP's plugin for scroll-based animations. It works great in React with proper setup. The key is registering the plugin, using the scope parameter to target the right elements, and being mindful of cleanup when components with ScrollTriggers unmount.
ScrollTrigger creates listeners on scroll events. When a component with ScrollTrigger unmounts, those listeners must be cleaned up. The useGSAP hook handles this automatically, but you need to be careful with route changes in Single Page Apps.
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useGSAP } from '@gsap/react';
import { useRef } from 'react';
gsap.registerPlugin(ScrollTrigger);
export default function ScrollReveal() {
const containerRef = useRef(null);
useGSAP(() => {
// Animate elements as they scroll into view
gsap.from('.reveal-item', {
opacity: 0,
y: 50,
duration: 0.8,
stagger: 0.2,
ease: 'power2.out',
scrollTrigger: {
trigger: '.reveal-item',
start: 'top 75%', // When top of element is 75% down viewport
end: 'top 25%', // When top of element is 25% down viewport
toggleActions: 'play none none none',
markers: false // Set to true for debugging
}
});
}, { scope: containerRef });
return (
<div ref={containerRef}>
<div className="reveal-item">Item 1</div>
<div className="reveal-item">Item 2</div>
<div className="reveal-item">Item 3</div>
</div>
);
}
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useGSAP } from '@gsap/react';
import { useRef } from 'react';
gsap.registerPlugin(ScrollTrigger);
export default function ParallaxEffect() {
const layerRef = useRef(null);
useGSAP(() => {
// Scrub: tie animation progress to scroll position
gsap.to(layerRef.current, {
y: -100, // Move up 100px
ease: 'none', // No easing for scrub
scrollTrigger: {
trigger: layerRef.current,
start: 'top center',
end: 'bottom center',
scrub: 1, // 1 second smoothing
markers: false
}
});
});
return (
<div
ref={layerRef}
style={{
width: '100%',
height: '200px',
backgroundColor: 'rgba(0,255,102,0.1)',
borderRadius: '8px'
}}
>
Scroll to see parallax
</div>
);
}
In Next.js with Next/Link or other client-side routing, ScrollTriggers need special handling. When users navigate to a new page, refresh ScrollTrigger by calling ScrollTrigger.refresh() after new content mounts. This is especially important if your page length changes.
For comprehensive ScrollTrigger coverage, see our complete ScrollTrigger guide. ScrollTrigger is incredibly powerful but also has nuances with pinning, refreshing, and complex layouts.
Timelines are essential for coordinating multiple animations. In React, you typically create timelines inside useGSAP and store them in refs if you need to control them (play/pause/reverse). This pattern lets you sequence complex animations while respecting React's lifecycle.
A common pattern is storing the timeline in a ref so you can control it from event handlers. This is particularly useful for interactive animations where users trigger play/pause or reverse actions.
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useRef } from 'react';
export default function SequenceAnimation() {
const tl = useRef(null);
const box1Ref = useRef(null);
const box2Ref = useRef(null);
const box3Ref = useRef(null);
const { contextSafe } = useGSAP(() => {
// Create timeline with paused state
tl.current = gsap.timeline({ paused: true });
// Add animations in sequence
tl.current
.to(box1Ref.current, { x: 100, duration: 0.6 }, 0)
.to(box2Ref.current, { y: -50, duration: 0.6 }, 0.2)
.to(box3Ref.current, { rotation: 360, duration: 0.6 }, 0.4);
});
// Control timeline with buttons
const handlePlay = contextSafe(() => tl.current.play());
const handlePause = contextSafe(() => tl.current.pause());
const handleReverse = contextSafe(() => tl.current.reverse());
return (
<div>
<div ref={box1Ref} className="box">Box 1</div>
<div ref={box2Ref} className="box">Box 2</div>
<div ref={box3Ref} className="box">Box 3</div>
<div style={{ marginTop: '20px' }}>
<button onClick={handlePlay}>Play</button>
<button onClick={handlePause}>Pause</button>
<button onClick={handleReverse}>Reverse</button>
</div>
</div>
);
}
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useRef, useState } from 'react';
export default function StateTriggeredAnimation() {
const tl = useRef(null);
const menuRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
useGSAP(() => {
tl.current = gsap.timeline({ paused: true });
tl.current
.to(menuRef.current, { opacity: 1, duration: 0.3 })
.to(menuRef.current, { x: 0, duration: 0.4 }, 0);
// Play or reverse based on isOpen state
if (isOpen) {
tl.current.play();
} else {
tl.current.reverse();
}
}, [isOpen]); // Re-run when isOpen changes
return (
<>
<button onClick={() => setIsOpen(!isOpen)}>
{isOpen ? 'Close' : 'Open'} Menu
</button>
<div ref={menuRef} style={{ opacity: 0, x: '-100%' }}>
<a href="#">Home</a>
<a href="#">About</a>
<a href="#">Contact</a>
</div>
</>
);
}
Always create timelines with { paused: true } when you plan to control them manually. Store timeline references in useRef, never directly in state. Use dependency arrays to re-trigger timelines when state changes. This pattern gives you full control over complex animation sequences.
Next.js 13+ uses Server Components by default. Since GSAP manipulates the DOM and requires browser APIs, all GSAP code must run on the client. Use the 'use client' directive to mark components as client components.
The pattern is simple: mark any component that uses GSAP with 'use client' at the top. If you're using Next.js App Router with client components that import GSAP dynamically, everything works seamlessly.
'use client'; // Mark as client component
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useRef } from 'react';
export default function HeroAnimation() {
const textRef = useRef(null);
useGSAP(() => {
gsap.from(textRef.current, {
opacity: 0,
y: 50,
duration: 1,
ease: 'power2.out'
});
});
return (
<h1 ref={textRef}>
Welcome to my Next.js site
</h1>
);
}
// app/layout.tsx
import type { Metadata } from 'next';
import { HeroAnimation } from '@/components/HeroAnimation';
export const metadata: Metadata = {
title: 'My Site',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html>
<body>
{/* Server Component */}
{/* Client Component with GSAP */}
<HeroAnimation />
{children}
</body>
</html>
);
}
import dynamic from 'next/dynamic';
// Lazy load component with animations
// Splits into separate bundle for better performance
const AnimatedComponent = dynamic(
() => import('@/components/AnimatedComponent'),
{ ssr: false } // Don't render on server
);
export default function Page() {
return (
<main>
<h1>My Page</h1>
<AnimatedComponent />
</main>
);
}
Always use 'use client' when importing GSAP. For heavy animations, use dynamic imports with ssr: false to keep server bundle size small. This separates animation code into client-only bundles, improving performance. ScrollTrigger works great in Next.js—just remember to refresh it on route changes if needed.
Next.js works beautifully with GSAP. The main rule: mark components using GSAP with 'use client' and you're good to go. The rest of your application can stay server-rendered.
Here are real-world animation patterns you'll use repeatedly in React projects. Master these and you can build almost any animation interaction.
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useRef, useState } from 'react';
export default function Modal() {
const overlayRef = useRef(null);
const modalRef = useRef(null);
const [isOpen, setIsOpen] = useState(false);
const { contextSafe } = useGSAP();
useGSAP(() => {
if (isOpen) {
// Animate in
gsap.to(overlayRef.current, {
opacity: 1,
duration: 0.3
});
gsap.to(modalRef.current, {
opacity: 1,
scale: 1,
duration: 0.4,
ease: 'back.out'
}, 0);
} else {
// Animate out
gsap.to(overlayRef.current, {
opacity: 0,
duration: 0.2
});
gsap.to(modalRef.current, {
opacity: 0,
scale: 0.8,
duration: 0.3,
ease: 'back.in'
}, 0);
}
}, [isOpen]);
const handleClose = contextSafe(() => {
setIsOpen(false);
});
return (
<>
<button onClick={() => setIsOpen(true)}>Open Modal</button>
<div
ref={overlayRef}
onClick={handleClose}
style={{ opacity: 0, position: 'fixed', inset: 0 }}
/>
<div
ref={modalRef}
style={{ opacity: 0, scale: 0.8, position: 'fixed' }}
>
Modal Content
</div>
</>
);
}
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useRef } from 'react';
export default function ProductList({ products }) {
const containerRef = useRef(null);
useGSAP(() => {
// Animate all product items with stagger
gsap.from('.product-item', {
opacity: 0,
y: 20,
duration: 0.6,
stagger: 0.1,
ease: 'power2.out'
});
}, { scope: containerRef, dependencies: [products] });
return (
<div ref={containerRef}>
{products.map((product) => (
<div key={product.id} className="product-item">
<h3>{product.name}</h3>
<p>${product.price}</p>
</div>
))}
</div>
);
}
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useRef, useState, useEffect } from 'react';
export default function ResponsiveAnimation() {
const boxRef = useRef(null);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
// Check if mobile
const checkMobile = () => setIsMobile(window.innerWidth < 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
useGSAP(() => {
if (isMobile) {
// Mobile animation (less intense)
gsap.from(boxRef.current, {
opacity: 0,
x: 20,
duration: 0.4,
ease: 'power1.out'
});
} else {
// Desktop animation (more complex)
gsap.from(boxRef.current, {
opacity: 0,
x: -100,
rotation: -45,
duration: 0.8,
ease: 'power2.out'
});
}
}, [isMobile]);
return <div ref={boxRef}>Responsive Box</div>;
}
When animations might re-run, kill existing animations first to prevent conflicts: gsap.killTweensOf(el). This ensures clean state when dependencies change or users trigger the same animation multiple times quickly.
GSAP is highly optimized, but there are patterns that make animations even better. Following these practices ensures smooth 60fps animations and clean React code.
Animate x, y, rotation, scale, and opacity instead of top/left/width/height. Transforms are GPU-accelerated and much faster. GSAP automatically uses transforms under the hood for these properties.
Store timeline and animation references in useRef, not state. Changing state causes re-renders which you don't need. Use contextSafe for event handlers to prevent memory leaks without state updates.
Check prefers-reduced-motion media query to disable animations for users who prefer them. This is a simple CSS check that shows respect for accessibility.
Always use useGSAP, never raw useEffect for GSAP. Use scope to isolate selectors. Wrap event handlers with contextSafe. These patterns prevent bugs and memory leaks.
Don't animate in loops without cleanup. Don't use document.querySelector globally (use scope instead). Don't forget contextSafe for events. Don't animate too many elements at once on mobile.
Use Chrome DevTools Performance tab to monitor frame rate. Target 60fps for smooth animations. On mobile, test on real devices—simulators don't show true performance.
import gsap from 'gsap';
import { useGSAP } from '@gsap/react';
import { useRef, useState, useEffect } from 'react';
export default function AccessibleAnimation() {
const elementRef = useRef(null);
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false);
useEffect(() => {
// Check for prefers-reduced-motion
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
setPrefersReducedMotion(mediaQuery.matches);
const listener = (e) => setPrefersReducedMotion(e.matches);
mediaQuery.addEventListener('change', listener);
return () => mediaQuery.removeEventListener('change', listener);
}, []);
useGSAP(() => {
if (prefersReducedMotion) {
// Skip animation, just set final state
gsap.set(elementRef.current, { opacity: 1, y: 0 });
} else {
// Play animation
gsap.from(elementRef.current, {
opacity: 0,
y: 50,
duration: 1,
ease: 'power2.out'
});
}
}, [prefersReducedMotion]);
return <div ref={elementRef}>Accessible animation</div>;
}
In development, React 18 Strict Mode intentionally double-invokes effects to catch bugs. useGSAP handles this properly—animations won't double-play. If using raw useEffect, you'll see double animations. This is why useGSAP is strongly recommended.
useEffect and kill animations on unmount. The @gsap/react package (specifically the useGSAP hook) handles all this automatically and is much cleaner. It also handles React 18 Strict Mode properly.
useRouter from Next.js to detect navigation and trigger exit animations. Alternatively, use layout transitions with framer-motion or other libraries. Another approach: animate content in and out as users navigate between pages, using dynamic route segments to control what renders.
ssr: false to keep your server bundle small. This is the recommended approach for Next.js 13+.
useRef<HTMLDivElement>(null) and get full intellisense. The @gsap/react package provides proper types for useGSAP hooks and contextSafe function.
You've learned everything needed to integrate GSAP into your React and Next.js projects. From basic animations with useGSAP to complex interactive sequences with ScrollTrigger, you're equipped to create engaging, performant animations that users will love.