gsap-react
Installation
SKILL.md
GSAP React Integration
GSAP integrates seamlessly with React through the @gsap/react package and useGSAP() hook. Automatically handles cleanup, scoping, and lifecycle management.
Installation
npm install gsap @gsap/react
Basic Setup
Register Hook
import gsap from 'gsap'
import { useGSAP } from '@gsap/react'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger, useGSAP)
Basic useGSAP Hook
import { useGSAP } from '@gsap/react'
import { useRef } from 'react'
function MyComponent() {
const container = useRef()
useGSAP(() => {
gsap.to('.box', { x: 200, duration: 1 })
}, { scope: container })
return <div ref={container}>
<div className="box">Animate me</div>
</div>
}
Selector Scoping
Scope to Container
import { useGSAP } from '@gsap/react'
import { useRef } from 'react'
function MyComponent() {
const container = useRef()
useGSAP(() => {
// Only targets within container
gsap.to('.box', { x: 200, duration: 1 })
}, { scope: container })
return (
<div ref={container}>
<div className="box">Box 1</div>
<div className="box">Box 2</div>
</div>
)
}
Without Scoping (Global)
useGSAP(() => {
// Targets globally (be careful!)
gsap.to('.box', { x: 200, duration: 1 })
})
Dependency Array
Rerun on Dependencies
import { useState } from 'react'
import { useGSAP } from '@gsap/react'
function Counter() {
const [count, setCount] = useState(0)
useGSAP(() => {
gsap.to('.counter', {
scale: 1 + count * 0.1,
duration: 0.5
})
}, { scope: container, dependencies: [count] })
return <div className="counter">{count}</div>
}
Automatic Cleanup
useGSAP(() => {
const tl = gsap.timeline()
tl.to('.box', { x: 200, duration: 1 })
// Automatically cleaned up on unmount
})
ScrollTrigger Integration
Basic ScrollTrigger
import { ScrollTrigger } from 'gsap/ScrollTrigger'
function ScrollComponent() {
const container = useRef()
useGSAP(() => {
gsap.to('.box', {
x: 500,
scrollTrigger: {
trigger: '.box',
start: 'top center',
end: 'bottom center',
scrub: true
}
})
}, { scope: container })
return <div ref={container}>
<div className="box">Scroll me</div>
</div>
}
Multiple ScrollTriggers
useGSAP(() => {
gsap.to('.box1', {
x: 200,
scrollTrigger: {
trigger: '.box1',
start: 'top 80%'
}
})
gsap.to('.box2', {
x: 200,
scrollTrigger: {
trigger: '.box2',
start: 'top 80%'
}
})
}, { scope: container })
Pinning with useGSAP
useGSAP(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.section',
start: 'top top',
end: '+=500',
pin: true,
scrub: true
}
})
tl.to('.box', { x: 200, duration: 1 })
.to('.box', { y: 200, duration: 1 })
}, { scope: container })
Context Safety
contextSafe for Events
import { useGSAP } from '@gsap/react'
import { useRef } from 'react'
function Button() {
const container = useRef()
useGSAP(() => {
const { contextSafe } = useGSAP(() => {}, { scope: container })
const handleClick = contextSafe(() => {
gsap.to('.box', {
scale: 1.5,
duration: 0.3,
ease: 'back.out(1.7)'
})
})
document.querySelector('.button').addEventListener('click', handleClick)
return () => {
document.querySelector('.button').removeEventListener('click', handleClick)
}
})
return <div ref={container}>
<div className="button">Click me</div>
<div className="box">Box</div>
</div>
}
Responsive with matchMedia
Basic matchMedia
useGSAP(() => {
const mm = gsap.matchMedia()
mm.add('(min-width: 768px)', () => {
gsap.to('.box', { x: 200, duration: 1 })
})
mm.add('(max-width: 767px)', () => {
gsap.to('.box', { x: 100, duration: 1 })
})
return () => mm.revert()
}, { scope: container })
Nested matchMedia
useGSAP(() => {
const mm = gsap.matchMedia()
mm.add('(min-width: 1024px)', () => {
gsap.to('.box', { x: 300, duration: 1 })
})
mm.add('(min-width: 768px) and (max-width: 1023px)', () => {
gsap.to('.box', { x: 200, duration: 1 })
})
mm.add('(max-width: 767px)', () => {
gsap.to('.box', { x: 100, duration: 1 })
})
return () => mm.revert()
}, { scope: container })
Dynamic Content
Animate on Data Change
function DataList({ items }) {
const container = useRef()
useGSAP(() => {
gsap.from('.item', {
opacity: 0,
y: 20,
stagger: 0.1,
duration: 0.5
})
}, { scope: container, dependencies: [items] })
return (
<div ref={container}>
{items.map((item, i) => (
<div key={i} className="item">{item}</div>
))}
</div>
)
}
Animate on Mount
function Welcome() {
const container = useRef()
useGSAP(() => {
gsap.from('.welcome', {
opacity: 0,
y: -30,
duration: 0.8,
ease: 'back.out(1.7)'
})
}, { scope: container })
return <div ref={container}>
<div className="welcome">Welcome!</div>
</div>
}
Animate on Unmount
function Modal({ isOpen, onClose }) {
const container = useRef()
useGSAP(() => {
if (isOpen) {
gsap.from('.modal-content', {
scale: 0.8,
opacity: 0,
duration: 0.3,
ease: 'back.out(1.7)'
})
}
return () => {
if (isOpen) {
gsap.to('.modal-content', {
scale: 0.8,
opacity: 0,
duration: 0.2,
ease: 'power2.in'
})
}
}
}, { scope: container, dependencies: [isOpen] })
return isOpen ? (
<div className="modal" ref={container}>
<div className="modal-content">Modal content</div>
</div>
) : null
}
Advanced Patterns
Controlled Timeline
function ControlledAnimation({ isPlaying }) {
const container = useRef()
useGSAP(() => {
const tl = gsap.timeline({ paused: true })
tl.to('.box', { x: 200, duration: 1 })
.to('.box', { y: 200, duration: 1 })
.to('.box', { rotation: 360, duration: 1 })
// Expose timeline
window.myTimeline = tl
}, { scope: container })
useEffect(() => {
if (isPlaying && window.myTimeline) {
window.myTimeline.play()
} else if (!isPlaying && window.myTimeline) {
window.myTimeline.pause()
}
}, [isPlaying])
return <div ref={container}>
<div className="box">Box</div>
</div>
}
Flip Animation
import { Flip } from 'gsap/Flip'
gsap.registerPlugin(Flip)
function DraggableList({ items }) {
const container = useRef()
useGSAP(() => {
const state = Flip.getState('.item')
// DOM changes happen automatically by React
Flip.from(state, {
duration: 0.8,
ease: 'power2.out'
})
}, { scope: container, dependencies: [items] })
return (
<div ref={container}>
{items.map((item, i) => (
<div key={item.id} className="item">
{item.text}
</div>
))}
</div>
)
}
SplitText in React
import { SplitText } from 'gsap/SplitText'
gsap.registerPlugin(SplitText)
function AnimatedText({ text }) {
const container = useRef()
useGSAP(() => {
const split = new SplitText('.text', { type: 'chars' })
gsap.from(split.chars, {
opacity: 0,
y: 20,
stagger: 0.02,
duration: 0.5
})
return () => split.revert()
}, { scope: container, dependencies: [text] })
return (
<div ref={container}>
<div className="text">{text}</div>
</div>
)
}
ScrollTrigger + Dynamic Data
function InfiniteScroll({ data }) {
const container = useRef()
useGSAP(() => {
ScrollTrigger.refresh()
}, { scope: container, dependencies: [data] })
useGSAP(() => {
gsap.from('.item', {
opacity: 0,
y: 50,
stagger: 0.1,
duration: 0.6,
scrollTrigger: {
trigger: '.item',
start: 'top 80%',
toggleActions: 'play none none reverse'
}
})
}, { scope: container, dependencies: [data] })
return (
<div ref={container}>
{data.map((item, i) => (
<div key={i} className="item">{item}</div>
))}
</div>
)
}
Multiple Hooks
Separate Scopes
function MultiAnimation() {
const scope1 = useRef()
const scope2 = useRef()
useGSAP(() => {
gsap.to('.box1', { x: 200, duration: 1 })
}, { scope: scope1 })
useGSAP(() => {
gsap.to('.box2', { y: 200, duration: 1 })
}, { scope: scope2 })
return (
<>
<div ref={scope1}>
<div className="box1">Box 1</div>
</div>
<div ref={scope2}>
<div className="box2">Box 2</div>
</div>
</>
)
}
Strict Mode Handling
useGSAP Handles Strict Mode
// React 18 Strict Mode double-mounts components
// useGSAP automatically handles this
function StrictComponent() {
const container = useRef()
useGSAP(() => {
// Runs twice in Strict Mode, but cleans up properly
gsap.from('.box', {
opacity: 0,
y: 20,
duration: 0.5
})
}, { scope: container })
return <div ref={container}>
<div className="box">Box</div>
</div>
}
Performance Tips
Memoize Callbacks
import { useCallback } from 'react'
function OptimizedComponent() {
const container = useRef()
const animateBox = useCallback(() => {
gsap.to('.box', { x: 200, duration: 1 })
}, [])
useGSAP(() => {
animateBox()
}, { scope: container, dependencies: [animateBox] })
return <div ref={container}>
<div className="box">Box</div>
</div>
}
Throttle Expensive Updates
useGSAP(() => {
const throttledUpdate = gsap.utils.throttle(() => {
// Expensive operation
updateLayout()
}, 100)
gsap.to('.box', {
onUpdate: throttledUpdate
})
}, { scope: container })
Common Mistakes
1. Not Scoping Selectors
// ❌ Targets globally, can affect other components
useGSAP(() => {
gsap.to('.box', { x: 200 })
})
// ✅ Scope to container
useGSAP(() => {
gsap.to('.box', { x: 200 })
}, { scope: container })
2. Forgetting Dependencies
// ❌ Animation doesn't update when data changes
useGSAP(() => {
gsap.from('.item', { opacity: 0, stagger: 0.1 })
}, { scope: container })
// ✅ Include data in dependencies
useGSAP(() => {
gsap.from('.item', { opacity: 0, stagger: 0.1 })
}, { scope: container, dependencies: [items] })
3. Manual Cleanup Issues
// ❌ Memory leaks, zombie animations
useEffect(() => {
gsap.to('.box', { x: 200 })
}, [])
// ✅ useGSAP handles cleanup
useGSAP(() => {
gsap.to('.box', { x: 200 })
}, { scope: container })
Best Practices
- Always use scope - Prevents selector conflicts
- Include dependencies - Rerun animations when data changes
- Use contextSafe for events - Ensures cleanup
- Leverage matchMedia - Responsive animations
- Refresh ScrollTrigger - After dynamic DOM changes
- Use revertOnUpdate - Revert animations on updates
- Clean up timelines - Expose and kill if needed
- Test with Strict Mode - useGSAP handles double-mounts
Quick Reference
| Feature | Method |
|---|---|
| Import hook | import { useGSAP } from '@gsap/react' |
| Register hook | gsap.registerPlugin(useGSAP) |
| Basic usage | useGSAP(() => { ... }, { scope: ref }) |
| Dependencies | useGSAP(() => { ... }, { dependencies: [data] }) |
| ScrollTrigger | Works automatically with useGSAP |
| matchMedia | gsap.matchMedia().add(...) |
| Context safe | const { contextSafe } = useGSAP(...) |
| Cleanup | Automatic, or return cleanup function |
Weekly Installs
1
Repository
microck/gsap-skillsFirst Seen
6 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
warp1