play-state y la taquigrafía animation
La única de las ocho propiedades que controla el estado en vez de la definición, y las reglas exactas de orden y reseteo del atajo que hacen que a veces borre lo que acabas de escribir.
Siete de las ocho propiedades describen qué animación es. animation-play-state describe si está corriendo, y por ser una propiedad CSS y no un método hereda todo lo que sabe hacer la cascada: se puede cambiar con una clase, con :hover, con una consulta de medios o con un contenedor padre. Es el único punto de la API declarativa donde el estado de reproducción es cascadable. Y la taquigrafía que agrupa a las ocho tiene reglas de orden y de reseteo que, ignoradas, borran declaraciones que acabas de escribir.
- Pausar y reanudar animaciones desde la cascada sin JavaScript.
- Ordenar correctamente los valores de la taquigrafía
animation. - Evitar el conflicto entre un nombre de keyframes y una palabra clave.
- Colocar las longhands que el atajo resetearía.
Pausar es congelar el tiempo local
animation-play-state acepta running —el valor inicial— y paused. Al pausar, el tiempo local de la animación deja de avanzar y se conserva; al volver a running, continúa desde donde estaba. No se reinicia ni salta. Esto la distingue de quitar y volver a poner animation-name, que sí reinicia.
Que sea una propiedad y no un método tiene consecuencias que compensan sus limitaciones. La primera es el interruptor global, que no se puede escribir de otra forma:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-play-state: paused !important;
}
}
Un !important de autor está por encima del escalón de animaciones, así que esta regla gana a cualquier declaración de terceros. Es la única manera de detener animaciones de una librería que no controlas sin tocar su código.
La segunda es el control por interacción, sin una línea de JavaScript:
.carrusel { animation: desplazar 30s linear infinite; }
.carrusel:hover,
.carrusel:focus-within { animation-play-state: paused; }
Y la tercera, menos obvia: la pausa se hereda a través de la cascada del elemento, no del árbol. animation-play-state no es una propiedad heredada, así que pausar el padre no pausa las animaciones de los hijos. Para eso hace falta un selector descendente, * { animation-play-state: paused } dentro de un contenedor, o recurrir a document.getAnimations().
Pausar durante la fase previa detiene el propio retardo: la animación se queda esperando indefinidamente y mostrará el relleno backwards si lo tiene. Y no existe ningún evento de pausa: animationstart, animationiteration y animationend no se enteran de nada. Si necesitas reaccionar a la pausa, el estado real está en el objeto Animation correspondiente, no en los eventos.
Combinada con un retardo negativo, la pausa deja de ser una pausa y pasa a ser un selector de instante: es la técnica de convertir unos keyframes en una tabla de consulta que vimos en el retardo negativo.
El orden de la taquigrafía
La taquigrafía acepta los valores en casi cualquier orden. La forma canónica, y la que conviene escribir siempre para que sea legible, es esta:
.x {
animation: nombre duracion funcion retardo iteraciones direccion relleno estado;
/* ejemplo completo */
animation: aparecer 400ms cubic-bezier(0.2, 0, 0, 1) 120ms 1 normal backwards running;
}
La única restricción de orden real es la de los tiempos: el primer valor temporal es la duración y el segundo es el retardo. No hay ninguna otra forma de distinguirlos, porque ambos son del mismo tipo. animation: x 120ms 400ms dura 120 milisegundos y espera 400, que casi nunca es lo que se quería escribir.
Los demás valores se identifican por su tipo o por su palabra clave, así que animation: infinite 2s linear girar es válida y equivale a la forma canónica. Escribirla así es legal y una mala idea.
La ambigüedad interesante está en el nombre. Como animation-name acepta cualquier identificador, un nombre que coincida con una palabra clave de otra longhand se asigna a la otra longhand, no al nombre:
@keyframes reverse { from { opacity: 0; } to { opacity: 1; } }
.mal { animation: reverse 1s; } /* direction: reverse, sin animacion */
.bien { animation: 1s reverse; /* sigue mal, mismo problema */ }
.ok { animation-name: reverse; animation-duration: 1s; } /* funciona */
La lista de nombres prohibidos en la práctica incluye none, normal, reverse, alternate, alternate-reverse, infinite, paused, running, forwards, backwards, both, ease, ease-in, ease-out, ease-in-out, linear y step-start. Ninguno de esos sirve como nombre de keyframes si piensas usar la taquigrafía. Prefijar los nombres resuelve el problema de golpe y además evita las colisiones de nombres entre hojas de estilo.
Lo que el atajo borra
Como toda taquigrafía de CSS, animation resetea a su valor inicial todas las longhands que gobierna, incluidas las que no has escrito. Eso es lo esperable, pero produce un error muy concreto cuando combinas el atajo con longhands calculadas:
/* MAL: la taquigrafia resetea el retardo a 0 */
.item {
animation-delay: calc(var(--i) * 60ms);
animation: aparecer 400ms ease-out backwards;
}
/* BIEN: la longhand va despues */
.item {
animation: aparecer 400ms ease-out backwards;
animation-delay: calc(var(--i) * 60ms);
}
La regla es mecánica: la taquigrafía primero, las longhands calculadas después. Aparece constantemente en sistemas de componentes donde una clase base define la animación con el atajo y un modificador ajusta una sola longhand; si el modificador está en una regla que se aplica antes en el orden de la cascada, se pierde.
Y hay un caso que muerde y que no está en la lista de las ocho: el atajo animation también resetea animation-timeline a su valor inicial, aunque no acepte ningún valor para esa propiedad. Si declaras la línea de tiempo de una animación y después escribes la taquigrafía, la pierdes. La forma segura es la misma regla de siempre: el atajo primero, todo lo demás después.
Reiniciar una animación CSS desde JavaScript es un problema clásico sin solución evidente, y la razón es que animation-name no cambia de valor cuando lo vuelves a asignar al mismo nombre: el motor compara el valor computado, ve que es idéntico y no reinicia nada. Los tres apaños que circulan son quitar la clase, forzar un reflujo leyendo offsetWidth y volver a ponerla; asignar animation: none, forzar reflujo y restaurar; o duplicar la regla @keyframes con dos nombres y alternarlos. Los tres funcionan y los tres son horribles, porque dependen de provocar un cálculo de layout sincrónico a propósito. La solución real está en WAAPI: elemento.getAnimations().forEach(a => { a.currentTime = 0; a.play(); }) reinicia la animación CSS existente sin tocar clases, sin reflujo forzado y sin duplicar reglas. Es el primer caso, de muchos, en que el objeto Animation resuelve en una línea algo que en CSS puro no tiene arreglo limpio.
Varias animaciones y el atajo
La taquigrafía acepta listas separadas por comas, y cada grupo se parsea de forma independiente:
.doble {
animation:
aparecer 400ms ease-out backwards,
flotar 3s ease-in-out 400ms infinite alternate;
}
Esta forma es preferible a declarar ocho longhands con listas paralelas, porque mantiene cada animación en una línea legible y elimina la posibilidad de desalinear las listas. El precio es que no puedes calcular una longhand de una sola de las animaciones sin reescribir todas: animation-delay: 0ms, calc(var(--i) * 60ms) te obliga a repetir el valor de la primera.
Recuerda que cuando dos de esas animaciones tocan la misma propiedad, la última de la lista gana y la primera se descarta por completo. Componerlas en vez de pisarlas requiere animation-composition, y esa es una conversación distinta que se resuelve mejor desde el lado de JavaScript.
Escribe un carrusel con animation: desplazar 20s linear infinite que se pause en :hover. Después añade animation-duration: 40s en una clase modificadora colocada antes de la regla base y comprueba que se pierde. Muévela después y comprueba que funciona. Ese es todo el modelo del reseteo del atajo.