El objeto de stagger completo
amount frente a each, el punto de emanación, la distribución en rejilla con eje, la curva que reparte los tiempos, y la función de stagger para los casos que la configuración no cubre.
El objeto de stagger es una API pequeña con una densidad inusual: seis propiedades que, combinadas, generan prácticamente todos los patrones de aparición escalonada que se ven en interfaces modernas, incluidos los que emanan del centro de una rejilla o los que se propagan en diagonal. Y la propiedad más importante del conjunto es la primera decisión que hay que tomar —amount o each— porque de ella depende que la coreografía siga funcionando cuando la lista cambie de tamaño.
- Elegir entre
amountyeachsegún lo que tenga que permanecer constante. - Colocar el punto de emanación con las palabras clave y con índices.
- Distribuir sobre una rejilla y restringir la propagación a un eje.
- Escribir una función de stagger cuando la configuración declarativa no llegue.
amount contra each: la decisión que importa
Son mutuamente excluyentes y representan dos formas opuestas de pensar el reparto.
each es el tiempo entre el arranque de cada elemento. Es lo mismo que el stagger simple con un número. La duración total crece con el número de elementos.
amount es el tiempo total que se reparte entre todos los arranques. El hueco entre elementos se calcula dividiendo: con amount: 1 y cien elementos, hay 0,01 segundos entre cada uno.
// Hueco fijo: 20 elementos tardan 1.9 s en arrancar todos.
gsap.to('.item', { autoAlpha: 1, duration: 0.4, stagger: { each: 0.1 } });
// Total fijo: 20 o 200 elementos, el ultimo siempre arranca en el segundo 1.
gsap.to('.item', { autoAlpha: 1, duration: 0.4, stagger: { amount: 1 } });
La regla es directa: si la lista tiene un número variable de elementos, usa amount. Es lo único que garantiza que la coreografía dure lo mismo con tres tarjetas que con cincuenta, y es la diferencia entre una animación que se integra en una secuencia mayor y otra que la descuadra según los datos que llegue.
each es correcto cuando el ritmo entre elementos es la información: una lista de pasos que se van completando, donde el intervalo comunica el tiempo de cada paso.
La geometría del reparto
from: de dónde emana
from define desde qué punto de la lista se propaga el escalonado. Los elementos arrancan en orden de proximidad a ese punto.
Acepta un índice numérico o cinco palabras clave:
| Valor | Comportamiento |
|---|---|
"start" |
Desde el primero. Es el comportamiento por defecto |
"end" |
Desde el último hacia atrás |
"center" |
Desde el centro hacia los dos extremos |
"edges" |
Desde los dos extremos hacia el centro |
"random" |
Orden aleatorio, disponible desde la versión 3.1 |
| Un número | Desde el elemento de ese índice |
// Emanan desde el centro de la lista.
gsap.from('.tarjeta', {
scale: 0.8,
autoAlpha: 0,
duration: 0.5,
stagger: { amount: 0.8, from: 'center' },
});
// Desde el quinto elemento hacia fuera.
gsap.from('.tarjeta', { autoAlpha: 0, stagger: { each: 0.06, from: 4 } });
"center" y "edges" producen efectos perceptivamente muy distintos aunque sean simétricos. "center" se lee como algo que se despliega desde un origen; "edges" como algo que converge. Para una aparición, "center" casi siempre es mejor porque dirige la mirada hacia donde está el contenido principal.
Si has definido grid, from acepta además un array de dos ratios que indican la posición dentro de la rejilla: [0.5, 0.5] es el centro, [1, 0] es la esquina superior derecha, [0, 1] la inferior izquierda.
grid y axis: la propagación en dos dimensiones
Por defecto, la proximidad se mide sobre el array plano: el elemento 7 y el 8 son vecinos aunque en pantalla estén en filas distintas. Con grid, GSAP mide la distancia en dos dimensiones.
// Doce elementos en tres filas de cuatro.
gsap.from('.celda', {
scale: 0,
autoAlpha: 0,
duration: 0.5,
stagger: {
amount: 1,
grid: [3, 4], // [filas, columnas]
from: 'center',
},
});
El valor "auto" hace que GSAP deduzca filas y columnas midiendo la posición real de los elementos:
stagger: { amount: 1, grid: 'auto', from: 'center' }
Es lo que quieres en una rejilla responsive donde el número de columnas cambia con el ancho. Con una advertencia importante: el cálculo se hace una sola vez, cuando se crea el tween. Si el layout cambia después, los desfases no se recalculan. Para que un stagger en rejilla sobreviva a un cambio de tamaño hay que rehacerlo, y el patrón recomendado es tener la animación en una timeline, rebobinarla a cero, vaciarla y reconstruirla:
function rehacer() {
tl.time(0).clear();
tl.from('.celda', {
scale: 0,
autoAlpha: 0,
duration: 0.5,
stagger: { amount: 1, grid: 'auto', from: 'center' },
});
}
axis restringe la medición a un solo eje. Sin él, la distancia es la combinación de las dos dimensiones y el efecto es radial. Con axis: 'y', solo cuenta la fila, así que todos los elementos de una misma fila arrancan a la vez y el efecto es una cascada horizontal. Con axis: 'x', columnas enteras.
// Fila a fila, de arriba abajo.
stagger: { amount: 0.8, grid: 'auto', axis: 'y' }
// Columna a columna, desde el centro hacia los lados.
stagger: { amount: 0.8, grid: 'auto', axis: 'x', from: 'center' }
Las rejillas se asumen ordenadas de arriba a la izquierda hacia abajo a la derecha, como el texto que se ajusta al llegar al borde. Si tus elementos no están en una rejilla uniforme, ni grid: [f, c] ni grid: 'auto' van a acertar, y toca la función de stagger.
Tres configuraciones cubren casi todo lo que se ve por ahí. La onda diagonal es grid: 'auto' con from: 0 y sin axis, porque la distancia combinada desde la esquina crece en diagonal. La cortina es axis: 'y' con from: 'start'. Y la explosión desde un punto es grid: 'auto' con from: [0.5, 0.5] y un ease de reparto.
El ritmo y el comportamiento por elemento
ease: la curva que reparte los tiempos
ease dentro del objeto de stagger no es el ease de la animación de cada elemento: es la curva que decide cuándo arranca cada uno. Su valor por defecto es "none", que reparte los arranques uniformemente.
gsap.from('.item', {
y: 20,
autoAlpha: 0,
duration: 0.4,
ease: 'power2.out', // ease de cada elemento
stagger: { amount: 1, ease: 'power2.in' }, // ease del reparto
});
Con ease: 'power2.in' en el reparto, los primeros elementos salen muy juntos y los últimos se van separando; con power2.out, al revés. Es un control fino que se nota mucho en listas largas: un reparto con power1.out hace que la cascada arranque densa y se vaya deshilachando, lo cual se lee como energía que se disipa.
Los dos eases son independientes y confundirlos produce resultados desconcertantes, porque cambiar el ease del reparto no cambia cómo se mueve ningún elemento, solo cuándo empieza.
Callbacks y repetición por elemento
El objeto de stagger acepta la mayoría de las propiedades especiales de un tween, y cuando las pones ahí se aplican a cada subtween en lugar de al conjunto. Es la forma de reaccionar por elemento:
gsap.from('.item', {
autoAlpha: 0,
duration: 0.4,
stagger: {
each: 0.08,
onStart() {
// Se dispara una vez por elemento, cuando le toca a el.
this.targets()[0].classList.add('activo');
},
},
});
Y como vimos, repeat y yoyo dentro del stagger hacen que cada elemento repita por su cuenta manteniendo su desfase, que es cómo se construyen los patrones ondulatorios permanentes:
gsap.to('.punto', {
y: -12,
duration: 0.6,
ease: 'sine.inOut',
stagger: { each: 0.05, repeat: -1, yoyo: true },
});
Esas seis líneas producen la onda de puntos que se ve en la mitad de los indicadores de carga del mundo.
La función de stagger
Cuando la configuración no llega, stagger acepta una función que recibe el índice, el objetivo y la lista, y devuelve el retardo total desde el inicio, no el hueco respecto al anterior.
gsap.to('.caja', {
y: 100,
duration: 0.5,
stagger(index, target, list) {
// Retardo desde el principio, no desde el elemento anterior.
return Number(target.dataset.orden) * 0.1;
},
});
Es la salida para tres casos que la configuración no cubre: elementos que no están en una rejilla uniforme, orden definido por datos del propio marcado, y repartos con lógica condicional. Para el primer caso, la propia documentación de GSAP publica una función auxiliar que distribuye según la posición real medida de cada elemento.
Todo lo que hace el objeto de stagger lo hace por dentro una utilidad pública que puedes usar para cualquier otra cosa: gsap.utils.distribute. Acepta exactamente las mismas propiedades —base, amount, each, from, grid, axis, ease— y devuelve una función que, dado un índice, devuelve un valor repartido.
Eso significa que puedes escalonar cualquier propiedad, no solo el tiempo. Escalonar la escala para que los elementos del centro sean pequeños y los de los bordes grandes, escalonar el desenfoque para que aumente con la distancia al centro, escalonar el color: todo es la misma operación de reparto aplicada a otro valor.
gsap.to('.celda', {
scale: gsap.utils.distribute({ base: 0.5, amount: 2.5, from: 'center' }),
duration: 0.8,
stagger: { amount: 0.6, grid: 'auto', from: 'center' },
});Ese tween reparte dos cosas a la vez con el mismo criterio: el tiempo con stagger y la escala con distribute, ambos emanando del centro. El resultado —una rejilla que se despliega desde el centro con los elementos centrales más pequeños— es de esos efectos que parecen exigir código a medida y son ocho líneas. Y funciona igual sobre propiedades que no son CSS, porque distribute solo devuelve números.
Reproduce los tres efectos reconocibles sobre la misma rejilla de veinticuatro elementos: la onda diagonal, la cortina fila a fila y la explosión desde el centro. Después añade un cuarto que use distribute para que la escala final también dependa de la distancia al centro, y comprueba que ambos repartos usan la misma configuración de from.