Animación de Scroll (GSAP + ScrollTrigger + Lenis)
Si los skills oficiales de GreenSock (gsap-core, gsap-scrolltrigger, gsap-react, gsap-plugins) están instalados, úsalos para los detalles de la API; este skill cubre la configuración del proyecto, la integración y los errores que rompen sitios reales.
1. Instalación
npm i gsap lenis
npm i @gsap/react # Solo React / Next.js
Todos los plugins de GSAP (ScrollTrigger, SplitText, Flip, ScrollSmoother, MorphSVG, DrawSVG…) se incluyen en el paquete público gsap y son gratuitos para uso comercial. Impórtalos desde gsap/<NombrePlugin> y llama a gsap.registerPlugin(...) una vez.
2. Módulo de configuración único
Crea lib/motion/setup.(js|ts) (o animations/) que se importe una vez en el cliente:
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 usa segundos, Lenis prefiere ms
gsap.ticker.add(tick);
gsap.ticker.lagSmoothing(0);
return () => { gsap.ticker.remove(tick); lenis.destroy(); };
}
Usa o Lenis o GSAP ScrollSmoother, no ambos.
3. Integración con frameworks
React / Next.js (App Router) — los archivos que usen GSAP necesitan "use client". Siempre usa useGSAP (limpieza automática, sin doble ejecución en Strict Mode):
"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 }); // Los selectores están limitados a este componente
return <section ref={root}>…</section>;
}
- Manejadores de eventos que crean animaciones: envuélvelos con
contextSafedeuseGSAP. - Proveedor de smooth scroll: un componente cliente en el layout raíz que llame a
initSmoothScroll()en un efecto (oReactLenisdelenis/reactconautoRaf: falsey el ticker de GSAP). - Cambio de ruta en Next.js: scroll al inicio (
lenis.scrollTo(0, { immediate: true })) yScrollTrigger.refresh()después de que monte la nueva página.
Vue / Nuxt — crea en onMounted dentro de gsap.context(() => {…}, rootEl), llama a ctx.revert() en onUnmounted. Nuxt: plugin solo cliente (plugins/gsap.client.ts).
Astro / multi-página estática — un <script> en el layout; con View Transitions, reinicia en astro:page-load y mata los triggers en astro:before-swap.
Vanilla — un main.js que se ejecute después de que el DOM esté listo; agrupa por sección en gsap.context().
4. Reglas de ScrollTrigger que previenen el 90% de los errores
- Pon
scrollTriggeren una timeline o tween de nivel superior — nunca en tweens anidados dentro de una timeline. - Usa
ease: "none"conscrub. - El orden de creación importa: crea los triggers de arriba a abajo en el orden de la página (o establece
refreshPriority), especialmente con pinning. - Fija un contenedor, no el elemento que animas. No fijes dentro de un padre con
transform. EvitapinSpacing: falsea menos que sea intencional. - Usa valores de función (
x: () => …) +invalidateOnRefresh: truepara cualquier cosa que dependa del tamaño de la ventana. - Llama a
ScrollTrigger.refresh()después de que imágenes, fuentes o contenido dinámico cambien el layout (window.addEventListener("load", …),document.fonts.ready). - Depura con
markers: true— elimínalos antes de publicar. - Estados ocultos iniciales: usa
gsap.from/fromTooautoAlphaestablecidos por JS, para que el contenido sea visible sin JS. once: truepara revelaciones que no deben reproducirse de nuevo (la mayoría).
5. Responsive y movimiento reducido
Envuelve cada escena en 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; // layout estático, sin animación
if (c.isDesktop) { /* escenas fijas / horizontales */ }
if (c.isMobile) { /* solo apariciones simples */ }
});
matchMedia revierte automáticamente cuando las condiciones cambian — sin limpieza manual.
6. Rendimiento
- Anima
x/y/scale/rotation/autoAlpha(transformaciones + opacidad). Evita animarwidth/height/top/leftyfilter: blur()pesado en áreas grandes. - Prefiere una timeline por sección en lugar de docenas de triggers separados; usa
ScrollTrigger.batchpara muchas revelaciones idénticas. gsap.quickTopara efectos de seguimiento del puntero.- Imágenes grandes dentro de escenas con scrub: usa
sizesadecuados y evita escalar mapas de bits enormes en cada fotograma.
7. Verificación
Desplázate por toda la página arriba y abajo rápido; cambia el tamaño de la ventana; recarga a mitad de página; navega a otra página y regresa (SPA) — sin saltos, sin pines duplicados, sin errores en consola. Prueba con movimiento reducido activado, a 375px y con limitación de CPU.
Recetas para efectos comunes: ../motion-direction/references/patterns.md.