Duración, iteraciones y retardo positivo
Las tres propiedades que definen cuánto dura de verdad una animación, la duración activa como producto, y por qué el retardo no se acumula entre iteraciones.
Ocho propiedades gobiernan una animación CSS, y solo tienen sentido leídas como un sistema: cada una responde a una pregunta distinta sobre la misma línea temporal. Las tres primeras contestan cuánto dura un ciclo, cuántos ciclos hay y cuándo empieza el primero. De su producto sale la duración activa, que es la magnitud sobre la que operan todas las demás y la que casi nadie calcula bien cuando hay iteraciones fraccionarias de por medio.
- Calcular la duración activa a partir de la duración y el número de iteraciones.
- Predecir el efecto de un número de iteraciones fraccionario.
- Situar el retardo positivo como una fase previa, no como parte de la animación.
- Saber cuándo se disparan
animationstart,animationiterationyanimationend.
Duración de ciclo frente a duración activa
animation-duration mide una iteración, no la animación completa. Su valor inicial es cero, y ese detalle explica el fallo más frecuente de todos: si escribes la taquigrafía sin tiempo, animation: fundido; no hace nada visible porque cada ciclo dura cero segundos.
animation-iteration-count acepta un número no negativo o infinite. El producto de ambos es la duración activa, el tiempo real durante el cual la animación está produciendo valores:
.a { animation: pulso 400ms 3; } /* duracion activa: 1200ms */
.b { animation: pulso 400ms infinite; } /* duracion activa: infinita */
.c { animation: pulso 400ms 2.5; } /* duracion activa: 1000ms */
El número de iteraciones puede ser fraccionario, y no es una curiosidad: 2.5 significa dos ciclos completos y medio ciclo más, que termina exactamente en el punto medio del recorrido de keyframes. Es la forma limpia de decir “gira una vuelta y media” o “late dos veces y quédate en el punto alto”, sin duplicar keyframes ni encadenar animaciones.
Con 0 como número de iteraciones, la duración activa es cero. La animación no produce ningún valor durante la fase activa, pero sigue existiendo como animación: los eventos de inicio y fin se disparan, y el relleno se aplica si lo hay. Es un interruptor válido para desactivar una animación heredada sin tocar su nombre.
Una duración negativa es inválida y hace que se descarte esa declaración concreta, no la regla entera. Como el valor inicial es cero, el efecto práctico de un animation-duration: -1s mal escrito es exactamente el mismo que no declarar nada: silencio.
El retardo es una fase, no un desplazamiento
animation-delay con valor positivo introduce una fase previa antes de la fase activa. Durante esa fase la animación existe, está asociada al elemento y tiene un tiempo local negativo, pero no está en su ventana de actividad. Si aporta o no algún valor durante ese intervalo lo decide animation-fill-mode, no el retardo.
Es un detalle que importa porque cambia la forma de razonar. El retardo no “empuja” la animación hacia adelante en el tiempo como si fuera un setTimeout; abre un tramo con semántica propia, que es donde vive backwards. La animación no está esperando a existir, está existiendo en su fase previa.
El retardo se aplica una sola vez, al principio. No se intercala entre iteraciones. Un animation: parpadeo 200ms 1s infinite espera un segundo y después parpadea sin descanso cada doscientos milisegundos, para siempre. Si lo que quieres es una pausa entre repeticiones, el retardo no sirve: la pausa se construye dentro de los keyframes, dejando un tramo con valores idénticos.
/* Parpadea durante el 30% del ciclo y descansa el 70% restante */
@keyframes parpadeo-con-pausa {
0%, 15% { opacity: 1; }
7% { opacity: 0.2; }
30%, 100% { opacity: 1; }
}
.aviso { animation: parpadeo-con-pausa 2s infinite; }
Esta es la única forma correcta de espaciar repeticiones en CSS puro, y tiene la ventaja de que la pausa participa del ciclo: si cambias la duración, la proporción entre movimiento y descanso se mantiene.
Los tres eventos y sus instantes exactos
Las tres propiedades anteriores determinan cuándo se dispara cada evento, y los instantes no son los que la intuición sugiere.
animationstart se dispara al final del retardo, cuando arranca la fase activa, no cuando se aplica la animación al elemento. Con un retardo de dos segundos, el evento llega dos segundos tarde. Si necesitas saber que una animación se ha aplicado, el evento no te sirve: tienes que consultarlo con getAnimations().
animationiteration se dispara entre iteraciones, nunca al principio ni al final. Con tres iteraciones se dispara dos veces. Con una iteración no se dispara nunca, lo cual descarta el patrón de “contar iteraciones sumando eventos” salvo que ajustes el punto de partida.
animationend se dispara al terminar la fase activa. Con infinite no se dispara jamás, y con animation-fill-mode: forwards se dispara igualmente aunque el valor final siga aplicándose: el relleno es una fase posterior al final, no una prolongación de la fase activa.
const caja = document.querySelector('.caja');
let ciclos = 0;
caja.addEventListener('animationstart', () => { ciclos = 1; });
caja.addEventListener('animationiteration', () => { ciclos += 1; });
caja.addEventListener('animationend', (e) => {
console.log(`${e.animationName} completo tras ${ciclos} ciclos en ${e.elapsedTime}s`);
});
La propiedad elapsedTime del evento mide el tiempo que la animación llevaba corriendo, sin contar el retardo. En un animationend de una animación de 400ms con tres iteraciones vale 1.2. En un animationstart vale 0, salvo con retardo negativo, donde vale el valor absoluto del retardo, porque la animación ya arranca avanzada.
animationstart se dispara aunque la duración sea cero. Y ese caso, que parece degenerado, es la técnica que hace posible detectar la inserción de nodos en el DOM sin MutationObserver: declaras @keyframes nodo-insertado sin contenido, se lo aplicas con duración mínima al selector que te interesa, y escuchas animationstart en un ancestro. Cada elemento que entre en el DOM y case con el selector dispara el evento en el instante en que se le calcula el estilo, incluso si llega por una inserción de terceros que tú no controlas. Fue la base de librerías como insertionQuery mucho antes de que existieran los observadores, y sigue siendo la única forma de reaccionar a la aparición de un nodo con el selector como filtro, delegando el emparejamiento al motor de CSS en lugar de recorrer mutaciones en JavaScript. El coste es real: cada emparejamiento crea un objeto de animación. Úsalo con selectores estrechos.
Listas paralelas y la propiedad que manda
Las ocho propiedades aceptan listas separadas por comas, porque un elemento puede llevar varias animaciones a la vez. Lo que casi nadie sabe es que animation-name decide cuántas hay, y las demás listas se ajustan a esa longitud.
.multi {
animation-name: girar, latir, brillar;
animation-duration: 2s, 600ms; /* se repite: 2s, 600ms, 2s */
animation-iteration-count: infinite; /* se repite para las tres */
}
Si una lista es más corta, se repite cíclicamente hasta cubrir el número de nombres. Si es más larga, se trunca. No hay error ni aviso en ninguno de los dos casos. Este mecanismo es cómodo cuando lo usas a propósito y desconcertante cuando añades un nombre y de repente la tercera animación hereda la duración de la primera.
Cuando varias animaciones del mismo elemento tocan la misma propiedad, gana la última de la lista. El orden de composición de las animaciones CSS es el orden de animation-name, y la posterior sobrescribe a la anterior salvo que intervenga animation-composition. Es el motivo por el que superponer una animación de entrada y una de flotación sobre transform produce que solo se vea una: no se suman, se pisan.
Escribe una animación de 1s con animation-iteration-count: 2.5 y animation-fill-mode: forwards sobre una rotación de 0 a 360 grados. Predice en qué ángulo se queda antes de ejecutarla, y comprueba el elapsedTime del animationend y cuántos animationiteration recibes.