Retour au blog

GSAP dans React et Next.js : useGSAP, nettoyage et ScrollTrigger

GSAP dans React et Next.js : useGSAP, nettoyage et ScrollTrigger

5 octobre 2026 — Animation

Publié le

5 octobre 2026

Temps de lecture

3 min de lecture

Catégorie

Animation

Mots-clés

  • GSAP
  • React
  • Next.js
  • ScrollTrigger
  • Animation

(Intro)Comment utiliser GSAP dans React et l’App Router de Next.js sans fuites de mémoire ni animations en double. useGSAP, scope, contextSafe, ScrollTrigger et changements de page.

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

bash
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

components/hero.tsx
'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 :

  1. '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.
  2. scope: container. Les sélecteurs comme .hero-line ne ciblent que l’intérieur de ce composant : deux instances n’animent jamais les éléments l’une de l’autre.
  3. 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 :

tsx
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 :

tsx
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 :

components/reveal-section.tsx
'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

tsx
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 :

ts
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: 0 dans 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 :

tsx
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, rotation et opacity ; 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é

  • useGSAP avec un scope pour chaque composant animé
  • contextSafe pour 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-motion avec gsap.matchMedia()

Newsletter

Recevez les nouveaux articles par e-mail

Un court e-mail à chaque nouvelle publication. Pas de spam, et désinscription possible à tout moment.

Design system pour petites équipes : commencer par les tokens
Design system pour petites équipes : commencer par les tokens
Article suivant