# GSAP + React Patterns Complete GSAP patterns for React with useGSAP hook. Based on official GSAP documentation. ## Table of Contents 1. [useGSAP Fundamentals](#usegsap-fundamentals) 2. [ScrollTrigger in React](#scrolltrigger-in-react) 3. [Context Safe Functions](#context-safe-functions) 4. [Timeline Orchestration](#timeline-orchestration) 5. [Text Animations](#text-animations) 6. [Pin Sections](#pin-sections) 7. [Batch Animations](#batch-animations) 8. [SVG Animations](#svg-animations) ## useGSAP Fundamentals ### Basic Setup ```tsx 'use client' import { useRef } from 'react' import gsap from 'gsap' import { useGSAP } from '@gsap/react' import { ScrollTrigger } from 'gsap/ScrollTrigger' // Register once (usually in a shared lib file) gsap.registerPlugin(ScrollTrigger, useGSAP) ``` ### Basic Animation ```tsx function AnimatedBox() { const containerRef = useRef(null) useGSAP(() => { gsap.to('.box', { x: 360, rotation: 360, duration: 1 }) }, { scope: containerRef }) // scope limits selectors to container return (
Animated
) } ``` ### With Dependencies ```tsx function AnimatedCounter({ count }: { count: number }) { const containerRef = useRef(null) useGSAP(() => { gsap.from('.counter', { textContent: 0, duration: 1, snap: { textContent: 1 } }) }, { scope: containerRef, dependencies: [count], // Re-run when count changes revertOnUpdate: true // Cleanup before re-running }) return (
{count}
) } ``` ### Cleanup Function ```tsx useGSAP(() => { const animation = gsap.to('.box', { x: 100 }) return () => { // Custom cleanup (useGSAP auto-reverts GSAP objects) console.log('Component unmounting') } }, { scope: containerRef }) ``` ## ScrollTrigger in React ### Basic ScrollTrigger ```tsx function ScrollSection() { const containerRef = useRef(null) useGSAP(() => { gsap.from('.content', { opacity: 0, y: 50, duration: 1, scrollTrigger: { trigger: '.content', start: 'top 80%', end: 'top 30%', toggleActions: 'play none none reverse', // markers: true, // Debug only } }) }, { scope: containerRef }) return (
Scroll to reveal
) } ``` ### Scrub Animation ```tsx function ScrubSection() { const containerRef = useRef(null) useGSAP(() => { gsap.to('.progress', { scaleX: 1, ease: 'none', scrollTrigger: { trigger: containerRef.current, start: 'top top', end: 'bottom bottom', scrub: 0.3, // Smooth catch-up (seconds) } }) }, { scope: containerRef }) return (
) } ``` ### Pin Section ```tsx function PinnedSection() { const containerRef = useRef(null) useGSAP(() => { gsap.to('.pinned-content', { y: 200, opacity: 0.5, scrollTrigger: { trigger: containerRef.current, start: 'top top', end: '+=1000', // Pin for 1000px of scroll pin: true, scrub: true, anticipatePin: 1, // Prevents jump } }) }, { scope: containerRef }) return (
Pinned content
) } ``` ### Multiple ScrollTriggers ```tsx function MultipleAnimations() { const containerRef = useRef(null) useGSAP(() => { const sections = gsap.utils.toArray('.section') sections.forEach((section, i) => { gsap.from(section, { opacity: 0, y: 50, duration: 0.8, scrollTrigger: { trigger: section, start: 'top 80%', toggleActions: 'play none none reverse', } }) }) }, { scope: containerRef }) return (
{[1, 2, 3, 4].map(i => (
Section {i}
))}
) } ``` ## Context Safe Functions Use `contextSafe` for animations triggered by events (click, hover, etc.). ### Click Handler ```tsx function ClickAnimation() { const containerRef = useRef(null) const { contextSafe } = useGSAP({ scope: containerRef }) const handleClick = contextSafe(() => { gsap.to('.box', { rotation: '+=360', duration: 0.5, ease: 'power2.out' }) }) return (
Click to spin
) } ``` ### Hover Animation ```tsx function HoverCard() { const containerRef = useRef(null) const { contextSafe } = useGSAP({ scope: containerRef }) const handleMouseEnter = contextSafe(() => { gsap.to('.card-content', { y: -10, duration: 0.3 }) }) const handleMouseLeave = contextSafe(() => { gsap.to('.card-content', { y: 0, duration: 0.3 }) }) return (
Hover me
) } ``` ## Timeline Orchestration ### Basic Timeline ```tsx function TimelineAnimation() { const containerRef = useRef(null) useGSAP(() => { const tl = gsap.timeline({ scrollTrigger: { trigger: containerRef.current, start: 'top center', } }) tl.from('.title', { opacity: 0, y: 50, duration: 0.6 }) .from('.subtitle', { opacity: 0, y: 30, duration: 0.5 }, '-=0.3') .from('.cta', { opacity: 0, scale: 0.9, duration: 0.4 }, '-=0.2') .from('.decoration', { opacity: 0, x: -20, stagger: 0.1 }, '-=0.3') }, { scope: containerRef }) return (

Title

Subtitle

) } ``` ### Timeline with Controls ```tsx function ControlledTimeline() { const containerRef = useRef(null) const tl = useRef(null) useGSAP(() => { tl.current = gsap.timeline({ paused: true }) .to('.box', { x: 200, duration: 0.5 }) .to('.box', { y: 100, duration: 0.5 }) .to('.box', { rotation: 360, duration: 0.5 }) }, { scope: containerRef }) const play = () => tl.current?.play() const reverse = () => tl.current?.reverse() const restart = () => tl.current?.restart() return (
) } ``` ## Text Animations ### SplitText (Club GSAP) ```tsx import { SplitText } from 'gsap/SplitText' gsap.registerPlugin(SplitText) function TextReveal() { const textRef = useRef(null) const splitRef = useRef(null) useGSAP(() => { splitRef.current = new SplitText(textRef.current, { type: 'chars, words, lines', linesClass: 'overflow-hidden' }) gsap.from(splitRef.current.chars, { opacity: 0, y: 100, rotateX: -90, stagger: 0.02, duration: 0.8, ease: 'back.out(1.7)', scrollTrigger: { trigger: textRef.current, start: 'top 80%', } }) return () => splitRef.current?.revert() }) return

Animated Headline

} ``` ### Text Scramble ```tsx import { TextPlugin } from 'gsap/TextPlugin' gsap.registerPlugin(TextPlugin) function ScrambleText() { const textRef = useRef(null) useGSAP(() => { gsap.to(textRef.current, { duration: 2, text: { value: 'Final Text', delimiter: '', }, ease: 'none', scrollTrigger: { trigger: textRef.current, start: 'top 80%', } }) }) return Initial Text } ``` ### Line-by-Line Mask Reveal ```tsx function LineMaskReveal({ text }: { text: string }) { const containerRef = useRef(null) useGSAP(() => { gsap.from('.line-inner', { yPercent: 100, duration: 0.8, ease: 'power4.out', stagger: 0.1, scrollTrigger: { trigger: containerRef.current, start: 'top 80%', } }) }, { scope: containerRef }) return (
{text.split('\n').map((line, i) => (
{line}
))}
) } ``` ## Pin Sections ### Horizontal Scroll ```tsx function HorizontalScroll() { const containerRef = useRef(null) const wrapperRef = useRef(null) useGSAP(() => { const sections = gsap.utils.toArray('.panel') gsap.to(sections, { xPercent: -100 * (sections.length - 1), ease: 'none', scrollTrigger: { trigger: wrapperRef.current, pin: true, scrub: 1, snap: 1 / (sections.length - 1), end: () => '+=' + wrapperRef.current!.offsetWidth, } }) }, { scope: containerRef }) return (
{[1, 2, 3, 4].map(i => (
Panel {i}
))}
) } ``` ### Stacked Pin Sections ```tsx function StackedSections() { const containerRef = useRef(null) useGSAP(() => { const sections = gsap.utils.toArray('.stack-section') sections.forEach((section, i) => { ScrollTrigger.create({ trigger: section, start: 'top top', pin: true, pinSpacing: false, snap: 1, }) }) }, { scope: containerRef }) return (
{[1, 2, 3].map(i => (
Section {i}
))}
) } ``` ## Batch Animations ### Efficient Grid Animation ```tsx function BatchGrid() { const containerRef = useRef(null) useGSAP(() => { ScrollTrigger.batch('.grid-item', { onEnter: (elements) => { gsap.from(elements, { opacity: 0, y: 60, stagger: 0.1, duration: 0.6, ease: 'power3.out', }) }, start: 'top 85%', }) }, { scope: containerRef }) return (
{Array.from({ length: 16 }).map((_, i) => (
))}
) } ``` ## SVG Animations ### DrawSVG ```tsx import { DrawSVGPlugin } from 'gsap/DrawSVGPlugin' gsap.registerPlugin(DrawSVGPlugin) function DrawSVG() { const svgRef = useRef(null) useGSAP(() => { gsap.from('.draw-path', { drawSVG: '0%', duration: 2, ease: 'power2.inOut', stagger: 0.2, scrollTrigger: { trigger: svgRef.current, start: 'top 70%', } }) }) return ( ) } ``` ### MorphSVG ```tsx import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin' gsap.registerPlugin(MorphSVGPlugin) function MorphShape() { const shapeRef = useRef(null) const { contextSafe } = useGSAP() const morph = contextSafe(() => { gsap.to(shapeRef.current, { morphSVG: '#target-shape', duration: 1, ease: 'power2.inOut', }) }) return ( ) } ``` ## Important Notes ### React 18 Strict Mode Strict Mode calls effects twice in development. `useGSAP` handles this automatically with proper cleanup. ### SSR / Next.js Add `'use client'` to components using GSAP. The hook is SSR-safe. ### Refresh After Dynamic Content ```tsx useEffect(() => { ScrollTrigger.refresh() }, [items]) // After items change ``` ### Kill on Route Change ```tsx // In a layout or route component useEffect(() => { return () => { ScrollTrigger.getAll().forEach(t => t.kill()) } }, []) ```