Maîtriser GSAP de zéro : tweens, timelines, ScrollTrigger, plugins et intégration React/Next.js pour créer des animations web professionnelles.
GSAP fonctionne très bien avec React, mais il faut comprendre quelques règles. React gère le DOM différemment d'une page HTML statique — les composants montent, se démontent, et re-rendent. Sans cleanup correct, on accumule des animations orphelines qui causent des bugs impossibles à déboguer.
GreenSock fournit un package React officiel avec un hook useGSAP qui gère automatiquement le scope et le cleanup.
npm install gsap @gsap/react'use client'
import { useRef } from 'react'
import gsap from 'gsap'
import { useGSAP } from '@gsap/react'
gsap.registerPlugin(useGSAP)
export default function Hero() {
const containerRef = useRef<HTMLDivElement>(null)
useGSAP(() => {
gsap.from('.hero-title', {
y: 40,
opacity: 0,
duration: 0.8,
ease: 'power3.out',
})
gsap.from('.hero-sub', {
y: 20,
opacity: 0,
duration: 0.6,
delay: 0.2,
})
}, { scope: containerRef }) // ← scope au conteneur
return (
<div ref={containerRef}>
<h1 className="hero-title">Titre</h1>
<p className="hero-sub">Sous-titre</p>
</div>
)
}{ scope: containerRef } fait que les sélecteurs CSS dans le hook cherchent uniquement dans containerRef. Sans ça, .hero-title trouverait tous les éléments avec cette classe dans la page entière.
Le cleanup est automatique : quand le composant se démonte, useGSAP annule toutes les animations créées dans son callback et remet les éléments à leur état d'origine.
Enregistrer les plugins une seule fois, en dehors des composants.
// lib/gsap.ts — fichier dédié
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { useGSAP } from '@gsap/react'
gsap.registerPlugin(ScrollTrigger, useGSAP)
export { gsap, ScrollTrigger, useGSAP }// Dans les composants
import { gsap, ScrollTrigger, useGSAP } from '@/lib/gsap'Ne jamais appeler registerPlugin dans un composant React — il est appelé à chaque render, même si GSAP déduplique en interne.
Pour animer un élément précis (sans sélecteur CSS), utiliser useRef.
'use client'
import { useRef } from 'react'
import gsap from 'gsap'
import { useGSAP } from '@gsap/react'
export default function AnimatedCard() {
const cardRef = useRef<HTMLDivElement>(null)
const titleRef = useRef<HTMLHeadingElement>(null)
useGSAP(() => {
gsap.from(cardRef.current, {
y: 60,
opacity: 0,
duration: 0.7,
})
gsap.from(titleRef.current, {
x: -20,
opacity: 0,
duration: 0.5,
delay: 0.2,
})
})
return (
<div ref={cardRef} className="card">
<h2 ref={titleRef}>Titre</h2>
</div>
)
}useGSAP retourne une fonction contextSafe pour les animations déclenchées par des événements (clics, hover) — pas dans le setup initial.
'use client'
import { useRef } from 'react'
import gsap from 'gsap'
import { useGSAP } from '@gsap/react'
export default function Button() {
const btnRef = useRef<HTMLButtonElement>(null)
const { contextSafe } = useGSAP()
const handleClick = contextSafe(() => {
gsap.to(btnRef.current, {
scale: 0.95,
duration: 0.1,
yoyo: true,
repeat: 1,
})
})
return (
<button ref={btnRef} onClick={handleClick}>
Cliquer
</button>
)
}Sans contextSafe, les animations créées dans les event handlers ne sont pas associées au contexte GSAP du composant — elles ne seront pas nettoyées quand le composant se démonte.
'use client'
import { useRef } from 'react'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { useGSAP } from '@gsap/react'
gsap.registerPlugin(ScrollTrigger)
export default function Section() {
const sectionRef = useRef<HTMLElement>(null)
useGSAP(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: sectionRef.current,
start: 'top 75%',
toggleActions: 'play none none reverse',
},
})
tl.from('.section-title', { y: 30, opacity: 0, duration: 0.6 })
.from('.section-body', { y: 20, opacity: 0, duration: 0.5 }, '-=0.2')
}, { scope: sectionRef })
return (
<section ref={sectionRef}>
<h2 className="section-title">Titre</h2>
<p className="section-body">Contenu</p>
</section>
)
}useGSAP s'occupe du cleanup des ScrollTriggers aussi — ils sont détruits quand le composant se démonte.
GSAP est une bibliothèque client — elle a besoin du DOM. Next.js rend côté serveur, où document et window n'existent pas.
Règle n°1 : tous les composants GSAP sont 'use client'.
'use client' // ← obligatoire
import { useGSAP } from '@gsap/react'Règle n°2 : ne jamais appeler GSAP au niveau module dans un composant server.
// ❌ INTERDIT — exécuté côté serveur
import gsap from 'gsap'
gsap.to('.box', { opacity: 0 }) // crash : document is not defined
// ✅ CORRECT — dans un composant client
'use client'
import { useGSAP } from '@gsap/react'
useGSAP(() => {
gsap.to('.box', { opacity: 0 }) // exécuté seulement dans le navigateur
})Règle n°3 : importer les plugins dynamiquement si nécessaire.
Dans les cas rares où vous importez GSAP dans un contexte mixte client/serveur :
import dynamic from 'next/dynamic'
const AnimatedHero = dynamic(() => import('./AnimatedHero'), {
ssr: false, // ne rend pas côté serveur
})'use client'
import { useRef } from 'react'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { useGSAP } from '@gsap/react'
gsap.registerPlugin(ScrollTrigger)
export default function Features() {
const containerRef = useRef<HTMLDivElement>(null)
useGSAP(() => {
const mm = gsap.matchMedia()
mm.add('(prefers-reduced-motion: no-preference)', () => {
ScrollTrigger.batch('.feature-card', {
onEnter: (elements) => {
gsap.from(elements, {
y: 50,
opacity: 0,
duration: 0.7,
stagger: 0.12,
ease: 'power3.out',
})
},
start: 'top 85%',
})
})
}, { scope: containerRef })
return (
<div ref={containerRef} className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="feature-card">...</div>
<div className="feature-card">...</div>
<div className="feature-card">...</div>
</div>
)
}Sélecteur global sans scope :
// ❌ Sélectionne .title dans toute la page
useGSAP(() => {
gsap.from('.title', { opacity: 0 })
})
// ✅ Scoped au composant
useGSAP(() => {
gsap.from('.title', { opacity: 0 })
}, { scope: containerRef })Animer dans useEffect au lieu de useGSAP :
// ❌ Pas de cleanup automatique, pas de scope
useEffect(() => {
gsap.from('.box', { opacity: 0 })
}, [])
// ✅ useGSAP gère le cleanup
useGSAP(() => {
gsap.from('.box', { opacity: 0 })
}, { scope: ref })Ne pas enregistrer les plugins :
// ❌ ScrollTrigger non enregistré
import { ScrollTrigger } from 'gsap/ScrollTrigger'
// ... utiliser scrollTrigger: {} → erreur silencieuse
// ✅
gsap.registerPlugin(ScrollTrigger)GSAP dans React est propre dès qu'on utilise useGSAP correctement. Le dernier chapitre couvre les plugins avancés : TextPlugin, Flip, MotionPathPlugin, et un aperçu des plugins Club GSAP.