Dos reglas @keyframes con el mismo nombre
Las reglas de keyframes no se fusionan: gana una entera. Cómo se resuelve el nombre, qué papel juegan las capas, y en qué lugar exacto de la cascada se aplican las animaciones.
Si defines @keyframes fundido en dos ficheros distintos, no obtienes una regla combinada con los keyframes de ambos: obtienes una de las dos, entera, y la otra desaparece sin dejar rastro. Las reglas que definen nombres se resuelven con criterios propios, distintos de los de la cascada de declaraciones. Y una vez resuelta la regla, la animación se aplica en un escalón concreto de la cascada que explica por qué a veces tu style.opacity no hace absolutamente nada.
- Predecir qué regla
@keyframesgana cuando dos comparten nombre. - Explicar el papel de
@layeren la resolución de nombres. - Situar las animaciones en el orden de la cascada frente al autor y a
!important. - Diagnosticar por qué un estilo escrito desde JavaScript no surte efecto.
La resolución del nombre: gana una, no se mezclan
@keyframes pertenece a una familia de reglas que la especificación llama definidoras de nombre: junto con @property, @counter-style y @font-feature-values, no producen declaraciones sino que registran una entidad bajo un identificador. Y cuando dos reglas registran el mismo identificador, no se cascadan propiedad a propiedad: una sustituye a la otra por completo.
@keyframes fundido {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes fundido {
from { transform: scale(0.9); }
to { transform: scale(1); }
}
El resultado es una animación que solo escala. El opacity de la primera regla no está en ninguna parte: la segunda definición la reemplazó entera. Nada advierte de ello, ni en la consola ni en el inspector, donde solo verás la regla ganadora.
El criterio de desempate, dentro del mismo origen y la misma capa, es el orden en el documento: gana la última. Eso incluye el orden de las hojas de estilo, así que un fichero importado más tarde pisa a uno anterior, y un bundle cuyo orden de módulos cambia entre desarrollo y producción puede cambiar qué animación se ve. Es un tipo de bug particularmente desagradable porque solo aparece en el build.
Las capas invierten la intuición
Con @layer el criterio deja de ser el orden y pasa a ser la prioridad de capa. La regla ganadora es la de la capa de mayor prioridad, y solo dentro de la misma capa se recurre al orden en el documento.
@layer base, tema;
@layer tema {
@keyframes fundido { from { opacity: 0; } to { opacity: 1; } }
}
@layer base {
@keyframes fundido { from { transform: scale(0.9); } to { transform: scale(1); } }
}
Gana la de tema, aunque esté escrita antes, porque tema se declaró después en la lista de capas y por tanto tiene más prioridad. Y las declaraciones sin capa tienen más prioridad que cualquier capa, así que un @keyframes suelto en tu hoja principal gana a cualquiera definido dentro de una capa, escriba quien lo escriba y en el orden que sea.
Esto es exactamente lo contrario de lo que la gente espera de las capas, que suelen usarse para rebajar la prioridad del CSS de terceros. Con las declaraciones normales funciona así; con los nombres, no hay especificidad que valga y el mecanismo es puramente de capas y orden. Si metes una librería de animaciones en una capa baja pensando que podrás anular alguno de sus keyframes desde dentro de esa capa, descubrirás que no.
La consecuencia práctica es que los nombres de keyframes son un espacio global sin protección. Nombres como fade, spin, pulse o slide-in colisionan con casi cualquier librería. Prefija los tuyos con algo que no se repita, o genera los nombres desde el sistema de módulos si tu herramienta lo permite.
Dónde se aplican las animaciones en la cascada
Resuelto qué keyframes se usan, queda la otra mitad del problema: con qué fuerza compiten los valores que produce la animación contra el resto del CSS. La respuesta es que las animaciones ocupan un escalón propio, situado por encima de las declaraciones normales de autor y por debajo de las marcadas con !important.
De menor a mayor prioridad, el orden completo es: normales del navegador, normales del usuario, normales del autor, animaciones, importantes del autor, importantes del usuario, importantes del navegador, y por último transiciones.
Dos lecturas de esa lista. La primera: mientras una animación esté activa, sus valores ganan a cualquier declaración normal, venga de donde venga, con la especificidad que sea. Un #id.clase[attr] { opacity: 0.5 } pierde contra un keyframe del 50%. La especificidad no interviene, porque no están compitiendo en el mismo escalón.
La segunda: un !important de autor gana a la animación. Por eso animation-play-state: paused !important funciona como interruptor global y por eso una regla de accesibilidad con !important puede desactivar animaciones de terceros que no controlas.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Dentro de un bloque de keyframe, en cambio, !important se ignora: la declaración se descarta al parsear. Tiene sentido, porque una animación ya está por encima de las declaraciones normales y permitirle subir un escalón más rompería el propio orden que acabas de leer.
Ese escalón es la explicación del bug más frustrante de las animaciones CSS: una animación con animation-fill-mode: forwards no termina nunca desde el punto de vista de la cascada. Sigue en fase de relleno, sigue aportando su valor, y sigue estando por encima de las declaraciones normales de autor. Así que cuando después haces elemento.style.opacity = '0.5' desde JavaScript —que es una declaración normal de autor con la máxima especificidad posible— no pasa nada, y el inspector te muestra tu declaración aplicada, tachada, con la animación ganando. La solución no es subir especificidad, porque no hay especificidad que suba un escalón. Es quitar la animación: elemento.style.animation = 'none', o eliminar la clase que la aplica, o usar Animation.commitStyles() desde WAAPI para volcar el valor final al estilo en línea y después cancelar la animación. Comprobarlo es inmediato: si tachar la animación en el inspector hace que tu estilo aparezca, ya sabes qué escalón te está ganando.
Un nombre que no existe
Si animation-name apunta a un nombre para el que no hay ninguna regla @keyframes, no ocurre nada: no hay error, no hay aviso, la declaración es perfectamente válida y la animación sencillamente no produce ningún efecto. Es el mismo comportamiento que con animation-name: none.
Esto es intencionado y es lo que permite declarar animation antes de que la regla @keyframes exista, o en una hoja que se carga antes que la que la define. El orden entre la regla que usa un nombre y la que lo define es irrelevante: la resolución ocurre al aplicar el estilo, no al parsear.
También es lo que hace que un error tipográfico en el nombre sea invisible. Una animación que “no funciona” sin ningún síntoma —sin salto, sin flash, sin nada— casi siempre es un nombre mal escrito o una regla @keyframes que quedó fuera del bundle. Antes de tocar la curva, comprueba en el inspector que el elemento tiene una animación activa: si la pestaña de animaciones está vacía, el problema es el nombre.
Crea dos ficheros CSS, cada uno con un @keyframes girar distinto, y enlázalos en distinto orden en dos páginas. Comprueba que la animación cambia solo con el orden de los link. Después mete el segundo dentro de un @layer utilidades y observa que ahora pierde siempre, esté donde esté, porque el primero está sin capa.