Animer une interface avec GSAP et Next.js
Timelines, ScrollTrigger et budget performance : la méthode que j'applique pour des animations qui servent la lecture au lieu de la ralentir — et les pièges qui ne se voient qu'en production.
- Auteur
- Mario
- Publié le
- 30 juillet 2026
- Lecture
- 1 min
- Catégorie
- Développement

Une animation réussie ne se remarque pas. Elle guide le regard, explique une transition, absorbe une latence — et disparaît. Sur un site Next.js, la difficulté n'est pas d'écrire la timeline : c'est de la faire cohabiter avec le rendu serveur, le cycle de vie React et la contrainte de performance.
Voici la méthode que j'applique sur mes projets, du choix de la librairie jusqu'au budget d'images par frame.
Pourquoi GSAP plutôt que CSS seul
Les transitions CSS couvrent 80 % des besoins : un hover, un fondu, un déplacement simple. Dès qu'il faut séquencer plusieurs éléments, revenir en arrière, ou synchroniser une animation sur la position de scroll, elles montrent leurs limites.
- Timelines — orchestrer dix éléments avec des chevauchements précis reste lisible.
- ScrollTrigger — scrubber une animation sur le scroll, épingler une section, déclencher à l'entrée dans le viewport.
- Interruptibilité —
quickToréécrit la cible d'un tween en cours sans à-coup, indispensable pour un suivi de curseur.
La règle que je m'impose : si une transition CSS suffit, elle gagne. GSAP entre en jeu quand il y a une séquence ou un lien au scroll.
Le hook useGSAP, et pourquoi il évite les fuites
En React, une animation créée dans un useEffect survit au démontage du composant si personne ne la tue. En développement, le double montage du Strict Mode crée alors deux timelines sur le même élément, qui se battent pour la même propriété.
useGSAP résout ça : toutes les animations créées dans son callback sont enregistrées dans un contexte, et ce contexte est révoqué au démontage.
useGSAP(
() => {
gsap.from("[data-word]", { yPercent: 115, stagger: 0.07 });
},
{ scope: container, dependencies: [items.length] },
);
Le scope est le second réflexe à prendre : les sélecteurs sont résolus à l'intérieur du conteneur. Deux instances du même composant sur une page n'animent plus les éléments l'une de l'autre.
Scrubber une animation sur le scroll
L'effet le plus rentable visuellement : lier la progression d'une animation à la position de scroll plutôt qu'à une durée. L'utilisateur pilote, le mouvement devient tangible.
gsap.fromTo(
frame,
{ clipPath: "inset(0% 14% 0% 14% round 10px)" },
{
clipPath: "inset(0% 0% 0% 0% round 0px)",
ease: "none",
scrollTrigger: { trigger: section, start: "top 82%", end: "top 22%", scrub: 0.6 },
},
);
Deux détails font toute la différence. D'abord ease: "none" : avec un scrub, c'est le scroll qui porte le rythme, une courbe d'easing par-dessus donne une sensation de flottement. Ensuite clip-path plutôt que width : on masque au lieu de redimensionner, donc aucun recalcul de mise en page à chaque frame.
Le budget performance
Une animation à 60 fps dispose de 16 ms par frame. Tout ce qui déclenche un layout ou un paint mange ce budget.
- N'animer que
transform,opacityetclip-path. Jamaiswidth,topoumargin. - Poser
will-change: transformuniquement sur les éléments réellement animés — en abuser sature la mémoire GPU. - Sur mobile, couper les parallaxes : le gain visuel ne compense pas le coût.
Et le garde-fou non négociable : respecter prefers-reduced-motion. Pour une partie des utilisateurs, le mouvement provoque un vrai inconfort physique.
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) {
gsap.set(targets, { opacity: 1, y: 0 });
return;
}
Attention au piège : si l'état final n'est pas explicitement posé dans cette branche, les éléments animés en from() restent invisibles pour ces utilisateurs.
L'écueil de l'onglet en arrière-plan
Un détail qui coûte cher et qu'on ne voit jamais en développement. Quand une page est ouverte dans un onglet inactif, le navigateur gèle requestAnimationFrame. Une timeline de chargement ne progresse donc pas, et tout ce qui a été animé en from({ opacity: 0 }) reste à zéro.
La parade est simple : réserver la timeline de chargement au contenu visible d'emblée, et déclencher le reste avec un ScrollTrigger à l'entrée dans le viewport.
gsap.from("[data-meta]", {
opacity: 0,
y: 18,
scrollTrigger: { trigger: "[data-meta-grid]", start: "top 90%" },
});
Le contenu sous la ligne de flottaison n'est alors jamais animé tant qu'il n'est pas réellement à l'écran — et ne peut plus rester bloqué invisible.
Sujets abordés
- Next.js
- React
- TypeScript
- GSAP