Scroll Animation (GSAP + ScrollTrigger + Lenis)
Si les compétences officielles de GreenSock (gsap-core, gsap-scrolltrigger, gsap-react, gsap-plugins) sont installées, utilisez-les pour les détails de l'API ; cette compétence couvre la configuration du projet, l'intégration et les erreurs qui cassent les sites en production.
1. Installation
npm i gsap lenis
npm i @gsap/react # React / Next.js uniquement
Tous les plugins GSAP (ScrollTrigger, SplitText, Flip, ScrollSmoother, MorphSVG, DrawSVG…) sont fournis dans le package public gsap et sont gratuits pour un usage commercial. Importez depuis gsap/<NomPlugin> et appelez gsap.registerPlugin(...) une seule fois.
2. Un module de configuration unique
Créez lib/motion/setup.(js|ts) (ou animations/) qui est importé une fois côté client :
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import Lenis from "lenis";
import "lenis/dist/lenis.css";
gsap.registerPlugin(ScrollTrigger, SplitText);
export function initSmoothScroll() {
if (matchMedia("(prefers-reduced-motion: reduce)").matches) return null;
const lenis = new Lenis();
lenis.on("scroll", ScrollTrigger.update);
const tick = (time) => lenis.raf(time * 1000); // GSAP time is seconds, Lenis wants ms
gsap.ticker.add(tick);
gsap.ticker.lagSmoothing(0);
return () => { gsap.ticker.remove(tick); lenis.destroy(); };
}
Utilisez soit Lenis soit GSAP ScrollSmoother, pas les deux.
3. Intégration framework
React / Next.js (App Router) — les fichiers utilisant GSAP doivent avoir "use client". Utilisez toujours useGSAP (nettoyage automatique, pas de double exécution en mode strict) :
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(useGSAP, ScrollTrigger);
export function Features() {
const root = useRef<HTMLElement>(null);
useGSAP(() => {
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
gsap.from(".feature", {
autoAlpha: 0, y: 32, stagger: 0.08, ease: "power3.out",
scrollTrigger: { trigger: root.current, start: "top 75%" },
});
});
}, { scope: root }); // les sélecteurs sont limités à ce composant
return <section ref={root}>…</section>;
}
- Gestionnaires d'événements qui créent des animations : enveloppez avec
contextSafedeuseGSAP. - Fournisseur de défilement fluide : un composant client dans la mise en page racine qui appelle
initSmoothScroll()dans un effet (ouReactLenisdelenis/reactavecautoRaf: falseet le ticker GSAP). - Changement de route dans Next.js : défiler vers le haut (
lenis.scrollTo(0, { immediate: true })) etScrollTrigger.refresh()après le montage de la nouvelle page.
Vue / Nuxt — créez dans onMounted à l'intérieur de gsap.context(() => {…}, rootEl), appelez ctx.revert() dans onUnmounted. Nuxt : plugin côté client (plugins/gsap.client.ts).
Astro / site multi-pages statique — une balise <script> dans la mise en page ; avec View Transitions, réinitialisez sur astro:page-load et supprimez les triggers sur astro:before-swap.
Vanilla — un seul main.js qui s'exécute après le DOM ready ; regroupez par section dans gsap.context().
4. Règles ScrollTrigger qui évitent 90% des bugs
- Mettez
scrollTriggersur une timeline ou un tween de premier niveau — jamais sur des tweens imbriqués dans une timeline. - Utilisez
ease: "none"avecscrub. - L'ordre de création compte : créez les triggers de haut en bas dans l'ordre de la page (ou définissez
refreshPriority), surtout avec l'épinglage. - Épinglez un conteneur, pas l'élément que vous animez. N'épinglez pas à l'intérieur d'un parent avec
transform. ÉvitezpinSpacing: falsesauf si c'est intentionnel. - Utilisez des valeurs de fonction (
x: () => …) +invalidateOnRefresh: truepour tout ce qui dépend de la taille de la fenêtre. - Appelez
ScrollTrigger.refresh()après que les images/polices/contenu dynamique changent la mise en page (window.addEventListener("load", …),document.fonts.ready). - Déboguez avec
markers: true— supprimez avant la mise en production. - États initiaux cachés : utilisez
gsap.from/fromToouautoAlphadéfini par JS, pour que le contenu soit visible sans JS. once: truepour les révélations qui ne doivent pas se rejouer (la plupart d'entre elles).
5. Responsive & réduction de mouvement
Enveloppez chaque scène dans gsap.matchMedia() :
const mm = gsap.matchMedia();
mm.add({
isDesktop: "(min-width: 1024px)",
isMobile: "(max-width: 1023px)",
motionOK: "(prefers-reduced-motion: no-preference)",
}, ({ conditions: c }) => {
if (!c.motionOK) return; // mise en page statique, pas d'animation
if (c.isDesktop) { /* scènes épinglées / horizontales */ }
if (c.isMobile) { /* simples fondus vers le haut uniquement */ }
});
matchMedia se réinitialise automatiquement lorsque les conditions changent — pas de nettoyage manuel.
6. Performance
- Animez
x/y/scale/rotation/autoAlpha(transformations + opacité). Évitez d'animerwidth/height/top/leftet lesfilter: blur()lourds sur de grandes zones. - Préférez une timeline par section plutôt que des dizaines de triggers séparés ;
ScrollTrigger.batchpour de nombreuses révélations identiques. gsap.quickTopour les effets de suivi du pointeur.- Grandes images dans les scènes synchronisées : utilisez des
sizesappropriés, et évitez de redimensionner de gros bitmaps à chaque frame.
7. Vérification
Faites défiler toute la page rapidement vers le haut et vers le bas ; redimensionnez la fenêtre ; rechargez au milieu de la page ; naviguez ailleurs et revenez (SPA) — pas de sauts, pas d'épingles dupliquées, pas d'erreurs dans la console. Testez avec la réduction de mouvement activée, à 375px de largeur, et avec un ralentissement CPU.
Recettes pour les effets courants : ../motion-direction/references/patterns.md.