getAnimations: el inventario de todo lo que se mueve
Cómo enumerar las animaciones del documento o de un subárbol, qué tipos devuelve, en qué orden llegan y qué queda fuera de la lista.
La plataforma expone un inventario completo de lo que se está moviendo. document.getAnimations() devuelve todas las animaciones vivas del documento —las de CSS, las de las transiciones y las de JavaScript, sin distinción— y Element.getAnimations() hace lo propio con un elemento y opcionalmente con todo su subárbol. Es un punto de acceso sin equivalente en ninguna otra parte de la plataforma, y es lo que convierte la animación de una web en algo inspeccionable y gobernable desde fuera.
- Enumerar las animaciones de un documento, de un elemento y de un subárbol.
- Distinguir
Animation,CSSAnimationyCSSTransitionen los resultados. - Aprovechar que la lista viene en orden de composición.
- Saber qué animaciones no aparecen en la lista y por qué.
Las tres formas de preguntar
document.getAnimations(); // todo el documento
el.getAnimations(); // solo este elemento
el.getAnimations({ subtree: true }); // este elemento y sus descendientes
El resultado es siempre un array nuevo, una instantánea del momento. No es una lista viva: si creas una animación después, no aparece en el array que ya tenías. Y llamar dos veces devuelve dos arrays distintos con los mismos objetos dentro, así que comparar arrays con === no sirve pero comparar animaciones sí.
el.getAnimations() incluye las animaciones dirigidas a pseudo-elementos del elemento. Una animación creada con pseudoElement: '::before' aparece en la lista del elemento anfitrión, y se distingue por a.effect.pseudoElement, que vale la cadena del pseudo o null.
Los tres tipos que devuelve
for (const a of document.getAnimations()) {
if (a instanceof CSSTransition) {
console.log('transicion de', a.transitionProperty);
} else if (a instanceof CSSAnimation) {
console.log('animacion CSS', a.animationName);
} else {
console.log('animacion de JS', a.id || 'sin id');
}
}
CSSTransition y CSSAnimation heredan de Animation y añaden un solo campo cada una, que es justamente el que las identifica: la propiedad que se está transicionando y el nombre de la regla @keyframes. Todo lo demás —pause(), currentTime, playbackRate, finished— es idéntico.
Ese es el punto que hace útil el inventario: puedes tratar las tres por igual cuando quieres gobernarlas, y distinguirlas cuando quieres filtrar. Las utilidades típicas caben en tres líneas:
const soloJS = (as) => as.filter((a) => !(a instanceof CSSAnimation) && !(a instanceof CSSTransition));
const porId = (as, id) => as.filter((a) => a.id === id);
const porNombre = (as, n) => as.filter((a) => a.animationName === n);
Filtrar por id es lo más robusto para las tuyas, porque las animaciones CSS tienen id a cadena vacía y nunca colisionan con un identificador que tú hayas puesto.
El orden importa
La lista viene ordenada por orden de composición, de la que menos manda a la que más. Es decir: primero las transiciones, después las animaciones CSS, y al final las animaciones de JavaScript por orden de creación.
Eso convierte getAnimations() en la herramienta directa para diagnosticar el conflicto que abrió este nivel. Cuando dos animaciones tocan la misma propiedad y una no se ve, la respuesta está en el orden:
function quienManda(el, propiedad) {
return el.getAnimations()
.filter((a) => a.effect.getKeyframes().some((k) => propiedad in k))
.map((a) => ({
tipo: a.constructor.name,
id: a.id || a.animationName || a.transitionProperty,
composite: a.effect.composite,
estado: a.playState,
}));
}
quienManda(document.querySelector('.tarjeta'), 'transform');
La última entrada del array es la que gana, salvo que las de encima usen add o accumulate. Con esa función y treinta segundos, cualquier bug de “mi animación no se ve” queda resuelto sin abrir el depurador.
Qué no aparece
La lista contiene las animaciones relevantes, que la especificación define con precisión: las que están aplicando un efecto en ese momento, o las que lo aplicarán en el futuro. Quedan fuera dos grupos importantes.
Las animaciones en estado 'idle' no aparecen. Una animación construida con new Animation(...) y todavía sin reproducir no está en la lista, y una que has cancelado desaparece de ella. Es coherente: en 'idle' no aplica nada y no está programada para aplicar nada.
Las animaciones terminadas sin relleno tampoco aparecen. En cuanto pasan a 'finished' con fill: 'none', dejan de ser relevantes. Con fill: 'forwards' siguen apareciendo indefinidamente, y eso es precisamente lo que hace que el inventario crezca en una aplicación que crea animaciones rellenadas sin limpiarlas.
// Diagnostico de fuga de animaciones
setInterval(() => console.log(document.getAnimations().length), 2000);
Si ese número sube y no baja mientras el usuario interactúa, tienes animaciones con relleno acumulándose. Cada una participa en el cálculo de estilo de su elemento en cada frame, así que la degradación es progresiva y difícil de atribuir. Es una de las mediciones más baratas y más informativas que se pueden dejar puestas en desarrollo.
Un tercer grupo que no aparece: las animaciones de otros documentos. Un iframe es un documento distinto, con su propia línea de tiempo y su propio inventario, y document.getAnimations() del documento anfitrión no ve nada de lo que ocurre dentro. Para gobernar un iframe de mismo origen hay que llamar a frame.contentDocument.getAnimations().
getAnimations() es la única forma de descubrir que una transición CSS está en curso, y eso resuelve un problema clásico que no tiene otra solución fiable. El evento transitionend no se dispara si la transición se interrumpe, si la propiedad no llega a cambiar, o si el elemento se oculta a mitad; y transitionrun y transitioncancel ayudan pero exigen llevar la contabilidad a mano. Con el inventario, la pregunta “¿está este elemento en medio de una transición?” se responde sin estado externo: el.getAnimations().some(a => a instanceof CSSTransition). Y se puede ir más lejos: await Promise.all(el.getAnimations().map(a => a.finished)) espera a que todo lo que se esté moviendo en ese elemento termine, sean transiciones, animaciones CSS o animaciones de JavaScript, sin saber cuántas hay ni de qué tipo. Esa línea sustituye a los contadores de eventos que todo el mundo ha escrito alguna vez y que fallan cuando alguien añade una propiedad más a la lista de transition-property. Recuerda envolverla en el manejo del AbortError: si una de esas transiciones se cancela por un cambio de clase, la promesa se rechaza.
Un uso concreto: limpiar antes de animar
El patrón que evita la acumulación de animaciones superpuestas, escrito de una vez por todas:
function animarUnico(el, id, keyframes, opciones) {
for (const a of el.getAnimations()) {
if (a.id === id) a.cancel();
}
return el.animate(keyframes, { ...opciones, id });
}
Tres líneas que garantizan que cada elemento tiene como mucho una animación con ese identificador. Aplicado a un pointerenter que se dispara cincuenta veces, la diferencia entre usarlo y no usarlo es cincuenta objetos vivos frente a uno.
Fíjate en que el cancel() está filtrado por id. Cancelar todas las animaciones del elemento sería más corto y se llevaría por delante las transiciones CSS en curso y cualquier animación de otro módulo, que es exactamente el tipo de efecto colateral que hace que un componente rompa otro sin que nadie entienda por qué.
Abre cualquier web con animaciones y ejecuta en la consola document.getAnimations().map(a => [a.constructor.name, a.animationName ?? a.transitionProperty ?? a.id, a.playState]). Identifica cuántas son transiciones, cuántas animaciones CSS y cuántas de JavaScript. Después ejecuta document.getAnimations().forEach(a => a.pause()) y comprueba que todo se detiene a la vez, incluidas las de librerías de terceros que usen la API nativa.