GSAP React Integration
React-specific patterns for GSAP animations.
Quick Start
bash
npm install gsap @gsap/reacttsx
import { useGSAP } from '@gsap/react';import gsap from 'gsap';
function Component() { const containerRef = useRef(null);
useGSAP(() => { gsap.to('.box', { x: 200, duration: 1 }); }, { scope: containerRef });
return ( <div ref={containerRef}> <div className="box">Animated</div> </div> );}useGSAP Hook
Basic Usage
tsx
import { useGSAP } from '@gsap/react';import gsap from 'gsap';
function AnimatedComponent() { const container = useRef(null);
useGSAP(() => { // All GSAP animations here gsap.from('.item', { opacity: 0, y: 50, stagger: 0.1 }); }, { scope: container }); // Scope limits selector queries
return ( <div ref={container}> <div className="item">Item 1</div> <div className="item">Item 2</div> <div className="item">Item 3</div> </div> );}With Dependencies
tsx
function AnimatedComponent({ isOpen }) { const container = useRef(null);
useGSAP(() => { gsap.to('.drawer', { height: isOpen ? 'auto' : 0, duration: 0.3 }); }, { scope: container, dependencies: [isOpen] });
return ( <div ref={container}> <div className="drawer">Content</div> </div> );}Returning Context
tsx
function Component() { const container = useRef(null);
const { context, contextSafe } = useGSAP(() => { gsap.to('.box', { x: 200 }); }, { scope: container });
// Use contextSafe for event handlers const handleClick = contextSafe(() => { gsap.to('.box', { rotation: 360 }); });
return ( <div ref={container}> <div className="box" onClick={handleClick}>Click me</div> </div> );}Ref Patterns
Single Element Ref
tsx
function SingleElement() { const boxRef = useRef(null);
useGSAP(() => { gsap.to(boxRef.current, { x: 200, rotation: 360, duration: 1 }); });
return <div ref={boxRef}>Box</div>;}Multiple Element Refs
tsx
function MultipleElements() { const itemsRef = useRef([]);
useGSAP(() => { gsap.from(itemsRef.current, { opacity: 0, y: 30, stagger: 0.1 }); });
return ( <div> {[1, 2, 3].map((item, i) => ( <div key={item} ref={el => itemsRef.current[i] = el} > Item {item} </div> ))} </div> );}Dynamic Refs
tsx
function DynamicList({ items }) { const itemsRef = useRef(new Map());
useGSAP(() => { gsap.from(Array.from(itemsRef.current.values()), { opacity: 0, y: 20, stagger: 0.05 }); }, { dependencies: [items.length] });
return ( <div> {items.map(item => ( <div key={item.id} ref={el => { if (el) itemsRef.current.set(item.id, el); else itemsRef.current.delete(item.id); }} > {item.name} </div> ))} </div> );}Context and Cleanup
Automatic Cleanup
tsx
// useGSAP automatically cleans up animations on unmountfunction Component() { useGSAP(() => { // This timeline is automatically killed on unmount gsap.timeline() .to('.a', { x: 100 }) .to('.b', { x: 100 }); });}Manual Context (Without useGSAP)
tsx
import gsap from 'gsap';
function Component() { useEffect(() => { const ctx = gsap.context(() => { gsap.to('.box', { x: 200 }); gsap.to('.circle', { rotation: 360 }); });
return () => ctx.revert(); // Cleanup }, []);}Scoped Context
tsx
function Component() { const containerRef = useRef(null);
useEffect(() => { const ctx = gsap.context(() => { // Selectors only query within containerRef gsap.to('.item', { opacity: 1 }); }, containerRef);
return () => ctx.revert(); }, []);}Event Handlers
contextSafe for Events
tsx
function InteractiveComponent() { const container = useRef(null);
const { contextSafe } = useGSAP(() => { // Initial animation gsap.set('.box', { scale: 1 }); }, { scope: container });
const handleMouseEnter = contextSafe(() => { gsap.to('.box', { scale: 1.1, duration: 0.2 }); });
const handleMouseLeave = contextSafe(() => { gsap.to('.box', { scale: 1, duration: 0.2 }); });
return ( <div ref={container}> <div className="box" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > Hover me </div> </div> );}useCallback Alternative
tsx
function Component() { const boxRef = useRef(null); const tweenRef = useRef(null);
const animateBox = useCallback(() => { tweenRef.current?.kill(); tweenRef.current = gsap.to(boxRef.current, { x: '+=50', duration: 0.3 }); }, []);
useEffect(() => { return () => tweenRef.current?.kill(); }, []);
return <div ref={boxRef} onClick={animateBox}>Click</div>;}Timeline Management
Timeline Ref Pattern
tsx
function TimelineComponent() { const container = useRef(null); const tl = useRef(null);
useGSAP(() => { tl.current = gsap.timeline({ paused: true }) .to('.box', { x: 200 }) .to('.box', { y: 100 }) .to('.box', { rotation: 360 }); }, { scope: container });
const play = () => tl.current?.play(); const reverse = () => tl.current?.reverse(); const restart = () => tl.current?.restart();
return ( <div ref={container}> <div className="box">Animated</div> <button onClick={play}>Play</button> <button onClick={reverse}>Reverse</button> <button onClick={restart}>Restart</button> </div> );}Controlled Timeline
tsx
function ControlledAnimation({ progress }) { const container = useRef(null); const tl = useRef(null);
useGSAP(() => { tl.current = gsap.timeline({ paused: true }) .to('.element', { x: 500 }) .to('.element', { y: 200 }); }, { scope: container });
// Update timeline progress when prop changes useEffect(() => { if (tl.current) { tl.current.progress(progress); } }, [progress]);
return ( <div ref={container}> <div className="element">Controlled</div> </div> );}ScrollTrigger in React
Basic ScrollTrigger
tsx
import { useGSAP } from '@gsap/react';import gsap from 'gsap';import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
function ScrollComponent() { const container = useRef(null);
useGSAP(() => { gsap.from('.section', { opacity: 0, y: 100, scrollTrigger: { trigger: '.section', start: 'top 80%', toggleActions: 'play none none none' } }); }, { scope: container });
return ( <div ref={container}> <div className="section">Scroll to reveal</div> </div> );}ScrollTrigger Cleanup
tsx
function ScrollComponent() { const container = useRef(null);
useGSAP(() => { const triggers = [];
gsap.utils.toArray('.item').forEach(item => { const trigger = ScrollTrigger.create({ trigger: item, start: 'top 80%', onEnter: () => gsap.to(item, { opacity: 1 }) }); triggers.push(trigger); });
// Return cleanup function return () => triggers.forEach(t => t.kill()); }, { scope: container });}Custom Hooks
useAnimation Hook
tsx
function useAnimation(animation, deps = []) { const elementRef = useRef(null); const tweenRef = useRef(null);
useGSAP(() => { if (elementRef.current) { tweenRef.current = animation(elementRef.current); } return () => tweenRef.current?.kill(); }, { dependencies: deps });
return elementRef;}
// Usagefunction Component() { const boxRef = useAnimation((el) => gsap.from(el, { opacity: 0, y: 50, duration: 0.5 }) );
return <div ref={boxRef}>Animated</div>;}useFadeIn Hook
tsx
function useFadeIn(options = {}) { const { duration = 0.5, delay = 0, y = 30 } = options; const ref = useRef(null);
useGSAP(() => { gsap.from(ref.current, { opacity: 0, y, duration, delay, ease: 'power2.out' }); });
return ref;}
// Usagefunction Card() { const cardRef = useFadeIn({ delay: 0.2 }); return <div ref={cardRef}>Card content</div>;}useHoverAnimation Hook
tsx
function useHoverAnimation(enterAnimation, leaveAnimation) { const ref = useRef(null); const { contextSafe } = useGSAP({ scope: ref });
const onEnter = contextSafe(() => enterAnimation(ref.current)); const onLeave = contextSafe(() => leaveAnimation(ref.current));
return { ref, onMouseEnter: onEnter, onMouseLeave: onLeave };}
// Usagefunction Button() { const hoverProps = useHoverAnimation( (el) => gsap.to(el, { scale: 1.05, duration: 0.2 }), (el) => gsap.to(el, { scale: 1, duration: 0.2 }) );
return <button {...hoverProps}>Hover me</button>;}Temporal Collapse Patterns
Animated Countdown Digit
tsx
function CountdownDigit({ value, label }) { const digitRef = useRef(null); const prevValue = useRef(value);
useGSAP(() => { if (prevValue.current !== value) { gsap.timeline() .to(digitRef.current, { rotationX: -90, opacity: 0, duration: 0.25, ease: 'power2.in' }) .call(() => { digitRef.current.textContent = value; prevValue.current = value; }) .fromTo(digitRef.current, { rotationX: 90, opacity: 0 }, { rotationX: 0, opacity: 1, duration: 0.25, ease: 'power2.out' } ); } }, { dependencies: [value] });
return ( <div className="digit-container"> <span ref={digitRef} className="digit">{value}</span> <span className="label">{label}</span> </div> );}Cosmic Pulse Effect
tsx
function CosmicPulse({ children, color = '#00F5FF' }) { const containerRef = useRef(null);
useGSAP(() => { gsap.to(containerRef.current, { boxShadow: `0 0 30px ${color}, 0 0 60px ${color}`, duration: 1, repeat: -1, yoyo: true, ease: 'sine.inOut' }); }, { scope: containerRef });
return <div ref={containerRef}>{children}</div>;}Performance Tips
tsx
// 1. Use will-change for heavy animationsgsap.set('.animated', { willChange: 'transform' });
// 2. Batch similar animationsuseGSAP(() => { gsap.to('.item', { opacity: 1, stagger: 0.1 }); // Single tween // Not: items.forEach(item => gsap.to(item, ...)) // Multiple tweens});
// 3. Use refs over selectors for frequently animated elementsconst boxRef = useRef(null);gsap.to(boxRef.current, { x: 100 }); // Faster
// 4. Kill animations on rapid state changesconst tweenRef = useRef(null);useEffect(() => { tweenRef.current?.kill(); tweenRef.current = gsap.to(...);}, [dependency]);Reference
- See
gsap-fundamentalsfor animation basics - See
gsap-sequencingfor timeline composition - See
gsap-scrolltriggerfor scroll-based animations


