wandres.dev
GSAP VI · ScrollTrigger: el modelo

toggleActions: los cuatro cruces y las ocho respuestas

Qué significa cada una de las cuatro posiciones de toggleActions, las ocho palabras clave disponibles, las combinaciones que de verdad se usan, y once, toggleClass y preventOverlaps.

⏱ 18 min

toggleActions: "play none none none" es el valor por defecto y probablemente la cadena más copiada de todo GSAP sin que nadie sepa qué significan sus cuatro palabras. La estructura es sencilla en cuanto la ves: hay exactamente cuatro maneras de cruzar un intervalo, y esa cadena dice qué hacer en cada una. Elegir bien las cuatro es la diferencia entre una página que se siente coherente al subir y bajar y una en la que las animaciones se disparan al revés, se acumulan o simplemente desaparecen.

🎯 Al terminar esta lección sabrás
  • Enumerar los cuatro cruces posibles de un intervalo y en qué orden aparecen en la cadena.
  • Elegir entre las ocho palabras clave según lo que quieras que ocurra al volver hacia atrás.
  • Distinguir toggleActions de once, de toggleClass y de las llamadas de retorno.
  • Reconocer cuándo toggleActions no es la herramienta y hay que bajar a los callbacks.

Cuatro cruces, en este orden

Un intervalo tiene dos fronteras, y cada una se puede cruzar en dos sentidos. Eso da cuatro sucesos, y toggleActions los lista siempre en el mismo orden:

Posición Suceso Nombre del callback equivalente
Cruzas el start bajando onEnter
Cruzas el end bajando onLeave
Cruzas el end subiendo onEnterBack
Cruzas el start subiendo onLeaveBack

El orden no es alfabético ni casual: es el orden en que ocurren si haces scroll hasta abajo del todo y luego vuelves arriba. Entrando, saliendo, volviendo a entrar, volviendo a salir. Recordarlo así evita tener que consultar la tabla.

Cada posición admite una de estas ocho palabras: play, pause, resume, reset, restart, complete, reverse y none. Las diferencias importan y varias se confunden entre sí.

play continúa desde donde esté el cabezal hacia adelante. restart vuelve al principio y reproduce; no es lo mismo que play si la animación quedó a medias. resume reanuda en la dirección en que iba, que puede ser hacia atrás si venía de un reverse. pause congela. reset devuelve el cabezal al principio y pausa, dejando el elemento en su estado inicial. complete salta al final sin animar. reverse reproduce hacia atrás. none no hace nada.

Las combinaciones que se usan de verdad

Hay infinitas combinaciones y en la práctica se usan cuatro.

// 1) Una sola vez, la mas comun para revelados de contenido
toggleActions: "play none none none"

// 2) Simetrica: entra animando, sale desanimando. Ideal para elementos de UI
toggleActions: "play none none reverse"

// 3) Reproduce cada vez que aparece, siempre desde cero
toggleActions: "restart none none reset"

// 4) Reversible por los dos lados, para secciones que se cruzan en ambos sentidos
toggleActions: "play reverse play reverse"

La primera es el revelado clásico: el texto aparece y se queda. Es la que quieres para casi todo el contenido editorial, porque un párrafo que se desvanece al salir de pantalla y reaparece al volver es agotador de leer.

La segunda es la que quieres para elementos de interfaz: una barra de navegación que se encoge, un botón flotante que aparece, un fondo que cambia. Al subir deshace lo que hizo, que es lo que el usuario espera de un control.

La tercera reinicia cada vez. Se usa poco y casi siempre es un error: si el usuario sube y baja tres veces, ve la misma animación tres veces. Tiene sentido en una animación de datos que quieres que se pueda “volver a ver”, nunca en un revelado de texto.

La cuarta es la única que se comporta de forma completamente simétrica en las cuatro fronteras, y es la que necesitas cuando el intervalo es corto y el usuario puede quedarse oscilando en el borde.

⚠️
reverse y reset no son lo mismo, y la diferencia se ve al hacer scroll rápido

reverse reproduce hacia atrás respetando la duración y el ease; reset salta al estado inicial de golpe. Con un scroll lento la diferencia es estética. Con un scroll rápido, reverse deja la animación corriendo hacia atrás durante medio segundo mientras el usuario ya está tres secciones más abajo, y si vuelve a entrar en ese medio segundo, el play de la primera posición reanuda desde donde estuviera el cabezal. Es el origen del bug de “a veces el elemento se queda a medias”. Si tu animación es corta y el intervalo estrecho, reset es más predecible.

once, toggleClass y los límites del sistema

once: true no es una abreviatura de "play none none none", aunque el efecto visible se parezca. Hace tres cosas: fija toggleActions a ese valor, hace que onEnter se dispare como mucho una vez, y mata la instancia en cuanto se alcanza el final, retirando las escuchas de scroll y dejándola disponible para el recolector de basura. En una página con doscientos revelados de una sola vez, esa diferencia se nota: al llegar al pie, las doscientas instancias han desaparecido en vez de seguir comprobando su intervalo en cada tick.

gsap.from(".parrafo", {
  autoAlpha: 0,
  y: 24,
  duration: 0.5,
  stagger: 0.08,
  scrollTrigger: { trigger: ".articulo", start: "top 75%", once: true },
});

Ojo con un matiz: once no mata la animación asociada, solo la instancia. Si la animación estaba a medias en el momento de morir la instancia, sigue corriendo hasta acabar por su cuenta.

toggleClass vive en un plano paralelo y conviene no confundirlo. Añade y quita una clase cuando la instancia pasa de inactiva a activa y viceversa, y no obedece a toggleActions: no hay forma de decirle “pon la clase al entrar pero no la quites al salir”. Si necesitas ese control asimétrico, la herramienta son los callbacks.

// Forma corta: la clase va al propio trigger
toggleClass: "en-vista"

// Forma de objeto: la clase va a otros elementos
toggleClass: { targets: ".menu a", className: "resaltado" }

Y hay un ajuste que resuelve un problema concreto de secciones consecutivas: preventOverlaps. Cuando dos secciones adyacentes tienen animaciones de entrada y el usuario baja de golpe, la segunda arranca mientras la primera sigue corriendo y se ven las dos superpuestas. Con preventOverlaps: true, al ir a disparar una animación se fuerzan al final todas las anteriores basadas en scroll. Con una cadena arbitraria en lugar de true, el efecto se limita a las instancias que compartan esa misma cadena, lo que permite tener grupos independientes.

Su pareja natural es fastScrollEnd, que fuerza al final la animación actual si abandonas su zona por encima de cierta velocidad, 2500 píxeles por segundo por defecto. También acepta un número para fijar tu propio umbral.

ScrollTrigger.defaults({ preventOverlaps: true, fastScrollEnd: 3000 });
toggleActions codifica una decisión de diseño que casi nadie toma conscientemente: si tu página tiene memoria

Detrás de esas cuatro palabras hay una pregunta que rara vez se formula en voz alta, y que determina cómo se siente una página entera: ¿el scroll hacia arriba deshace el tiempo o solo cambia el punto de vista? Las dos respuestas son defendibles y producen productos distintos. Si eliges "play none none none" estás afirmando que el scroll es un recorrido con memoria: lo que ya ocurrió, ocurrió, y volver atrás te enseña el mundo tal como lo dejaste. Si eliges "play reverse play reverse" estás afirmando que el scroll es una cámara que se mueve sobre una escena estática, y que el estado de cada elemento es una función pura de la posición de la barra. La segunda opción es matemáticamente más limpia —el estado no depende del historial, solo de la posición— y es la que hace que una página sea reproducible: dos usuarios en la misma posición ven exactamente lo mismo. La primera es cognitivamente más amable, porque el contenido que ya has leído no se te desmonta delante al releerlo. El error no está en elegir una u otra; el error está en mezclarlas sin criterio dentro de la misma página, que es lo que ocurre por defecto cuando cada animación se configura copiando la de al lado. Una página donde los titulares tienen memoria y los fondos no, donde unos elementos se rebobinan y otros se quedan, produce una sensación de inestabilidad que nadie sabe señalar pero que todo el mundo nota. La regla que salva es aburrida: decide la política una vez para todo el proyecto, ponla en ScrollTrigger.defaults(), y trata cada desviación como una excepción que hay que justificar.

⚔️ Cuatro cruces, cuatro decisiones
  1. Monta una animación con marcadores y toggleActions: "play pause resume reset" y recorre las cuatro fronteras despacio hasta identificar visualmente cada una.
  2. Sustituye "play none none none" por once: true en un revelado y comprueba con ScrollTrigger.getAll().length cuántas instancias quedan vivas al llegar al pie.
  3. Provoca el bug de la animación a medias: intervalo corto, reverse en la cuarta posición, y scroll rápido oscilando en el borde. Arréglalo con reset.
  4. Pon dos secciones consecutivas con entradas de 0,8 segundos y baja de golpe. Añade preventOverlaps: true y compara.
  5. Define una política de toggleActions para todo tu proyecto con ScrollTrigger.defaults() y lista las excepciones que necesitas justificar.