GSAP reste la bibliothèque d’animation la plus complète du web, et depuis la version 3.13, tous les plugins — ScrollTrigger, SplitText, MorphSVG, tous — sont gratuits. Mais React affiche, réaffiche, démonte et (en développement) exécute les effets deux fois, et une bibliothèque qui manipule directement le DOM demande un peu de soin pour s’y intégrer.
Voici la configuration que j’utilise sur les projets Next.js App Router, ce site compris.
Installation
npm install gsap @gsap/react@gsap/react fournit le hook useGSAP, qui gère la partie où l’on se trompe le plus souvent : le nettoyage.
Le schéma de base
'use client'
import { useRef } from 'react'
import gsap from 'gsap'
import { useGSAP } from '@gsap/react'
gsap.registerPlugin(useGSAP)
export function Hero() {
const container = useRef<HTMLDivElement>(null)
useGSAP(
() => {
gsap.from('.hero-line', { yPercent: 100, opacity: 0, stagger: 0.08, duration: 0.9, ease: 'power3.out' })
},
{ scope: container },
)
return (
<div ref={container}>
<h1 className="hero-line">Design</h1>
<h1 className="hero-line">and build</h1>
</div>
)
}Il se passe trois choses :
'use client'. GSAP touche au DOM, donc le composant doit s’exécuter dans le navigateur. Gardez la partie animée réduite et laissez le reste de la page en Server Components.scope: container. Les sélecteurs comme.hero-linene ciblent que l’intérieur de ce composant : deux instances n’animent jamais les éléments l’une de l’autre.- Nettoyage automatique. Chaque tween, timeline et ScrollTrigger créé dans le callback est annulé au démontage du composant. C’est aussi pour cela que la double exécution de React en développement ne laisse pas d’animations en double.
Relancer lors d’un changement
Passez dependencies pour relancer quand les données changent, et revertOnUpdate si l’exécution précédente doit d’abord être annulée :
useGSAP(
() => {
gsap.to('.bar', { scaleX: progress, transformOrigin: 'left', duration: 0.4 })
},
{ scope: container, dependencies: [progress] },
)Gestionnaires d’événements : contextSafe
Les animations créées après l’exécution du hook — dans un gestionnaire de clic, par exemple — ne sont pas enregistrées automatiquement pour le nettoyage. Enveloppez ces fonctions dans contextSafe :
const { contextSafe } = useGSAP({ scope: container })
const onEnter = contextSafe((event: React.MouseEvent<HTMLElement>) => {
gsap.to(event.currentTarget, { scale: 1.04, duration: 0.3 })
})
return <button onMouseEnter={onEnter}>Hover me</button>ScrollTrigger
Enregistrez-le une fois (un appel au niveau du module suffit) et créez les déclencheurs dans useGSAP :
'use client'
import { useRef } from 'react'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { useGSAP } from '@gsap/react'
gsap.registerPlugin(useGSAP, ScrollTrigger)
export function RevealSection({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLElement>(null)
useGSAP(
() => {
gsap.utils.toArray<HTMLElement>('.reveal').forEach((el) => {
gsap.from(el, {
y: 40,
opacity: 0,
duration: 0.8,
ease: 'power2.out',
scrollTrigger: { trigger: el, start: 'top 85%' },
})
})
},
{ scope: ref },
)
return <section ref={ref}>{children}</section>
}Épinglage et scrub
useGSAP(
() => {
gsap.timeline({
scrollTrigger: {
trigger: '.story',
start: 'top top',
end: '+=200%',
scrub: 1, // follows the scrollbar, with 1 s of smoothing
pin: true, // holds the section while the timeline plays
},
})
.to('.story-image', { scale: 1.2 })
.to('.story-text', { yPercent: -50 }, '<')
},
{ scope: ref },
)Rafraîchir après un changement de mise en page
ScrollTrigger mesure les positions à sa création. Si des images, des polices ou du contenu tardif modifient ensuite la hauteur de la page, les positions de début et de fin se décalent. Appelez ScrollTrigger.refresh() une fois qu’ils sont chargés :
document.fonts.ready.then(() => ScrollTrigger.refresh())Donner des dimensions explicites aux images (ou un aspect-ratio) évite l’essentiel du problème dès le départ.
Pièges propres à Next.js
Changements de page et position de défilement
Avec l’App Router, naviguer d’une page à l’autre démonte les composants de l’ancienne page (donc useGSAP nettoie leurs déclencheurs) et monte les nouveaux. Si des déclencheurs se lancent au mauvais endroit après une navigation, c’est généralement que la nouvelle page a mesuré avant le chargement de ses images : rafraîchissez après le chargement, comme ci-dessus.
Les Server Components ne peuvent pas animer
Impossible de mettre useGSAP dans un Server Component. Au lieu de transformer toute une page en Client Component, enveloppez seulement la partie animée dans un petit wrapper client et passez-lui le contenu rendu côté serveur en children. Le contenu reste dans le HTML (bon pour le SEO et le LCP), et le mouvement s’ajoute par-dessus.
Éviter le flash de contenu non animé
gsap.from() s’exécute après l’hydratation : pendant une image, l’élément apparaît dans son état final, puis saute à l’état de départ. Deux solutions :
- Utiliser
gsap.fromTo()avec du CSS qui fixe l’état de départ (opacity: 0dans la feuille de style), en acceptant que l’élément dépende de JavaScript pour apparaître — uniquement pour les éléments non critiques. - Mieux pour tout ce qui est au-dessus de la ligne de flottaison : animer depuis un état visible (un léger décalage en
y, sans changement d’opacité), pour que le premier affichage soit déjà correct. Votre LCP vous dira merci — voir Core Web Vitals pour sites animés.
Accessibilité : matchMedia
gsap.matchMedia() permet de créer des animations uniquement quand une media query correspond, et les annule automatiquement quand ce n’est plus le cas :
useGSAP(
() => {
const mm = gsap.matchMedia()
mm.add('(prefers-reduced-motion: no-preference)', () => {
gsap.from('.card', { y: 60, opacity: 0, stagger: 0.1, scrollTrigger: '.cards' })
})
},
{ scope: container },
)Les visiteurs qui préfèrent réduire les animations obtiennent le contenu sans le mouvement. La même API gère les effets réservés au bureau ('(min-width: 768px)').
Conseils de performance
- Animez
x,y,scale,rotationetopacity; GSAP les convertit en transforms. - Utilisez
gsap.quickTo()pour les valeurs mises à jour en continu, comme un élément qui suit le curseur : il réutilise un seul tween au lieu d’en créer un à chaque événement. - Gardez un nombre raisonnable de ScrollTriggers simultanés ; regroupez les éléments similaires avec
ScrollTrigger.batch().
En résumé
useGSAPavec unscopepour chaque composant animécontextSafepour les animations créées dans les gestionnaires d’événements- Enregistrer les plugins une fois ; créer les ScrollTriggers dans
useGSAP - Rafraîchir ScrollTrigger après le chargement des polices et des images
- Garder le contenu dans des Server Components et l’animer via des wrappers client
- Respecter
prefers-reduced-motionavecgsap.matchMedia()

