wandres.dev
CONTROLAR ANIMACIONES CSS · Las ocho propiedades

El retardo negativo: la técnica del desfase

Un animation-delay negativo arranca la animación ya avanzada. Es la forma canónica de desfasar N elementos con una sola regla y de convertir unos keyframes en una tabla de consulta.

⏱ 18 min

El retardo negativo es la propiedad más infravalorada de la familia. No retrasa nada: adelanta. La animación arranca en el instante cero, pero ya avanzada la cantidad que le hayas indicado, como si llevara ese rato corriendo antes de que la vieras. Con eso se resuelven dos problemas que de otro modo exigen JavaScript: desfasar una lista de elementos con una única animación, y convertir una regla de keyframes en una función que puedes evaluar en cualquier punto.

🎯 Al terminar esta lección sabrás
  • Explicar qué hace exactamente un animation-delay negativo en la línea temporal.
  • Desfasar N elementos con una sola regla usando calc() y una variable de índice.
  • Construir un indicador de progreso estático con paused y retardo negativo.
  • Anticipar qué ocurre si el retardo negativo supera la duración activa.

Empezar por la mitad

Con animation-delay: -400ms sobre una animación de un segundo, en el instante en que la animación se aplica al elemento su tiempo local ya vale 400ms. Está en el 40% del recorrido, avanzando. No hay fase previa: la fase activa empieza inmediatamente, solo que empieza por dentro.

.barra { animation: recorrer 2s linear infinite; }
.barra.mitad { animation-delay: -1s; }   /* arranca en el punto medio */

Consecuencia directa: la animación termina antes. Con duración activa de 2s y retardo de -1s, el final llega un segundo después de aplicarla, no dos. El retardo negativo no desplaza la ventana, la recorta por la izquierda. Para animaciones con infinite eso no se nota porque no hay final; para animaciones de una sola pasada es exactamente lo que hace que sean útiles como continuación de otra cosa.

Como no existe fase previa, animation-fill-mode: backwards no tiene nada que rellenar y su efecto es nulo. Y el evento animationstart se dispara igualmente, en el instante cero, con un elapsedTime igual al valor absoluto del retardo. Es la única situación en la que ese campo no vale cero al arrancar.

Desfasar N elementos con una sola animación

Aquí está el uso canónico. Quieres una fila de barras que ondulen, o una lista de puntos de carga, o un teclado de columnas que suben y bajan. Todos hacen exactamente lo mismo, solo que cada uno va un poco por delante del anterior. La solución ingenua es escribir una animación por elemento, o generar animation-delay positivos con JavaScript. Ninguna de las dos hace falta.

@keyframes onda {
  0%, 100% { transform: scaleY(0.35); }
  50%      { transform: scaleY(1); }
}

.ecualizador span {
  transform-origin: bottom;
  animation: onda 1.2s ease-in-out infinite;
  animation-delay: calc(var(--i) * -120ms);
}
<div class="ecualizador">
  <span style="--i: 0"></span>
  <span style="--i: 1"></span>
  <span style="--i: 2"></span>
  <span style="--i: 3"></span>
  <span style="--i: 4"></span>
</div>

Una regla @keyframes, una declaración de animación, y cinco elementos moviéndose en fases distintas. Y el detalle que hace que esto sea la técnica correcta y no un apaño: como los retardos son negativos, todos los elementos están ya en marcha desde el primer frame. Con retardos positivos verías las barras arrancar una por una, en cascada, lo cual está bien para una entrada pero es exactamente lo que no quieres en un bucle continuo, donde el desfase debe estar establecido desde el principio.

Si no puedes poner el índice en un atributo de estilo, nth-child sirve igual de bien y es la opción cuando el número de elementos es fijo:

.ecualizador span:nth-child(1) { animation-delay: 0ms; }
.ecualizador span:nth-child(2) { animation-delay: -120ms; }
.ecualizador span:nth-child(3) { animation-delay: -240ms; }

Para el caso de un ciclo cerrado, elige el paso de forma que la suma de los desfases sea un múltiplo de la duración. Con cinco elementos y 1200ms de ciclo, un paso de 240ms reparte los cinco de forma perfectamente equidistante y el conjunto tiene periodo 1200ms. Con 120ms, los cinco ocupan solo la mitad del ciclo y el grupo se ve agrupado en un lado. El paso no es un número estético: es duración dividida entre número de elementos, o un divisor de ese cociente.

Los keyframes como tabla de consulta

El segundo uso es de otro orden. Si combinas un retardo negativo con animation-play-state: paused, la animación no avanza: se queda congelada exactamente en el punto que le indique el retardo. Los keyframes dejan de describir un movimiento y pasan a describir una función del progreso, que evalúas eligiendo el retardo.

@property --p { syntax: "<number>"; inherits: false; initial-value: 0; }

@keyframes escala-de-riesgo {
  0%   { background: #a6e3a1; }
  50%  { background: #f9e2af; }
  100% { background: #f38ba8; }
}

.medidor {
  animation: escala-de-riesgo 1s linear paused;
  animation-delay: calc(var(--p) * -1s);
}
<div class="medidor" style="--p: 0.72"></div>

Con --p entre 0 y 1, el elemento muestra el color correspondiente a ese punto de la escala, interpolado. No hay animación en marcha, no hay JavaScript, y la interpolación entre tres paradas de color la hace el motor. Cambiar --p desde JavaScript actualiza el color instantáneamente porque solo estás moviendo el punto de evaluación.

Esta técnica generaliza a cualquier cosa que los keyframes sepan interpolar: una posición en una ruta, un ángulo, un desenfoque, un conjunto de propiedades a la vez. Es la forma de expresar una interpolación multiparada en CSS puro, algo que ni color-mix() ni las funciones de rango cubren cuando hay más de dos extremos y varias propiedades acopladas.

Nivel dios

El retardo negativo no equivale a “pon animation-delay a cero y ajusta los keyframes”. Hay una diferencia observable con animation-direction: alternate: el retardo negativo posiciona el tiempo local dentro de la duración activa completa, iteraciones incluidas, no dentro de un ciclo. Con alternate y un retardo de -1.5s sobre ciclos de 1s, el elemento no arranca a mitad del primer ciclo: arranca a mitad de la segunda iteración, que va marcha atrás. Dos elementos con retardos de -0.25s y -1.25s no están desfasados medio segundo: están moviéndose en direcciones opuestas. Es la causa de que un ecualizador con alternate y desfases se vea desordenado cuando el paso no divide la duración del ciclo, y la razón por la que para desfasar conviene usar keyframes simétricos con normal en vez de alternate: con normal, el tiempo local es siempre el módulo de la duración de ciclo, y el desfase es trivialmente predecible.

Cuando el retardo se pasa de largo

Si el valor absoluto del retardo negativo iguala o supera la duración activa, la animación ya está terminada en el instante cero. Nunca la verás moverse.

/* Duracion activa 900ms, retardo -1s: nace muerta */
.tarde { animation: entrar 300ms 3 -1s; }

Con animation-fill-mode: none el elemento se ve con su estilo base y la animación no aporta nada. Con forwards se ve congelado en el estado final. El evento animationend se dispara igualmente, en el primer frame. Esto no es un caso teórico: aparece en cuanto construyes el retardo con calc() a partir de un índice y la lista crece más de lo previsto. Un calc(var(--i) * -200ms) sobre una animación de un segundo funciona hasta el quinto elemento y a partir del sexto los elementos aparecen ya terminados.

La protección es acotar el índice con un módulo, que en CSS puro no existe como función pero se puede aproximar con mod() si tu objetivo lo soporta, o simplemente reciclando índices en el marcado. Cuando el número de elementos es dinámico, la forma robusta es que el desfase sea siempre una fracción de la duración y no un valor absoluto: calc(var(--i) / var(--total) * -1.2s).

⚔️ Reto práctico

Monta el ecualizador de arriba con ocho barras. Prueba primero con paso -150ms sobre un ciclo de 1200ms, que reparte las ocho exactamente, y después con paso -100ms. Observa que en el segundo caso el conjunto tiene un frente visible que recorre las barras, y en el primero el movimiento parece aleatorio y sin dirección. Elegir el paso es elegir si el efecto es una ola o un ruido.