Qué pasa con una propiedad que falta en un keyframe
Cada propiedad animada tiene su propia lista de keyframes. Aprende a leer una regla como N funciones independientes en vez de como una secuencia de estados.
Si una regla @keyframes declara opacity en el 0% y en el 100%, pero solo declara transform en el 0%, el 50% y el 100%, no tienes una animación con tres estados: tienes dos animaciones independientes que comparten duración. Cada propiedad recorre su propia lista de puntos de control, con sus propios tramos y sus propias curvas. Leer una regla así en vez de como una secuencia es la diferencia entre predecir el resultado y sorprenderte con él.
- Descomponer una regla de keyframes en una lista de puntos por propiedad.
- Predecir la interpolación de una propiedad ausente en un keyframe intermedio.
- Explicar por qué el easing por tramo se resuelve también por propiedad.
- Detectar el bug clásico del
transformque se pierde a mitad de animación.
Una lista de puntos por propiedad
El modelo correcto es este: el navegador toma la regla y construye, para cada propiedad que aparezca en cualquier bloque, una lista ordenada de pares offset-valor formada solo por los bloques que sí declaran esa propiedad. Los bloques que no la mencionan no participan en absoluto en esa lista.
@keyframes desplegar {
0% { opacity: 0; transform: translateY(-12px) scaleY(0.9); }
30% { transform: translateY(0) scaleY(1.04); }
55% { transform: translateY(0) scaleY(0.98); }
100% { opacity: 1; transform: translateY(0) scaleY(1); }
}
La lista de opacity tiene dos puntos: 0% y 100%. La lista de transform tiene cuatro: 0%, 30%, 55% y 100%. La opacidad sube de forma continua y monótona durante toda la animación, sin enterarse de que existen los keyframes del 30% y del 55%. El transform hace su recorrido de cuatro tramos. Las dos cosas ocurren a la vez y no interactúan.
Esta separación es deliberada y es lo que hace las reglas legibles. Si tuvieras que repetir cada propiedad en cada keyframe, una animación de cinco propiedades y seis offsets serían treinta declaraciones, veintidós de ellas redundantes. La regla que permite omitir convierte esas treinta en las ocho que de verdad dicen algo.
El caso que rompe: una propiedad omitida por descuido
El problema aparece cuando la omisión no es intencionada. Es especialmente traicionero con transform, porque transform es una propiedad única cuyo valor es una lista de funciones, no varias propiedades independientes.
@keyframes mal {
0% { transform: translateX(0) rotate(0deg); }
50% { transform: rotate(180deg); } /* el translateX desaparece */
100% { transform: translateX(200px) rotate(360deg); }
}
En el 50% no hay translateX. Como transform es una sola propiedad, ese keyframe declara el valor completo rotate(180deg), que equivale a translateX(0) rotate(180deg). El elemento avanza hasta ninguna parte en la primera mitad y hace todo el recorrido horizontal en la segunda. Nadie ha omitido nada desde el punto de vista del motor: el keyframe del 50% está en la lista de transform y su valor es el que es.
Además, la interpolación entre listas de transform con distinto número de funciones no se hace componente a componente: cuando las listas no son compatibles, el navegador convierte ambas a matriz y interpola las matrices. Eso cambia visiblemente la trayectoria y puede introducir deformaciones intermedias que no estaban en ninguno de los dos extremos. Aquí las listas sí son compatibles por prefijo, pero en cuanto reordenas funciones entre keyframes entras en territorio de matrices.
La regla práctica es simple y no admite excepciones: si una propiedad aparece en varios keyframes, escríbela completa en todos ellos. Y si de verdad quieres que una parte del transform siga su propia línea temporal, esa parte va en una custom property registrada o en un elemento envolvente distinto, no en la misma lista.
El easing también se resuelve por propiedad
Como cada propiedad tiene su propia lista de tramos, la función de temporización que gobierna un tramo es la del keyframe que abre el tramo de esa propiedad, no la del keyframe más cercano en general.
@keyframes mezcla {
0% { opacity: 0; transform: scale(0.8);
animation-timing-function: linear; }
40% { transform: scale(1.1);
animation-timing-function: ease-out; }
100% { opacity: 1; transform: scale(1); }
}
La opacidad tiene un solo tramo, de 0% a 100%, abierto por el keyframe del 0%, así que usa linear durante toda la animación. El animation-timing-function: ease-out del 40% no la toca, porque el 40% no está en la lista de la opacidad. El transform tiene dos tramos: el primero con linear y el segundo con ease-out.
Una consecuencia útil: puedes dar a dos propiedades curvas completamente distintas sobre la misma duración sin tocar la declaración del elemento, simplemente eligiendo en qué keyframes declara cada una y qué función de temporización lleva cada uno de esos keyframes.
El keyframe implícito que el navegador sintetiza cuando falta el 0% o el 100% se genera por propiedad y solo si esa propiedad lo necesita. En la regla desplegar de arriba, si borras la opacity del 0%, la lista de la opacidad pasa a tener un solo punto explícito —el 100%— y el navegador sintetiza el 0% con la opacidad computada del elemento. Pero el transform no se ve afectado en absoluto: sigue con sus cuatro puntos. Esto significa que una misma regla puede tener, a la vez, propiedades ancladas a valores absolutos y propiedades que parten del estado del elemento. Es el mecanismo detrás de las animaciones de entrada reutilizables: fijas el opacity de 0 a 1 y dejas el transform sin 100% para que el elemento aterrice en el transform que ya tuviera. Nadie documenta esto porque suena a detalle, y es lo que hace que una librería de animaciones de entrada funcione sobre elementos que ya están transformados.
Leer una regla ajena
Cuando te toque depurar una regla que no escribiste, el procedimiento es mecánico. Haz una tabla: una fila por propiedad, una columna por offset, y marca dónde aparece cada una.
| propiedad | 0% | 30% | 55% | 100% |
|---|---|---|---|---|
opacity |
sí | no | no | sí |
transform |
sí | sí | sí | sí |
filter |
no | sí | no | sí |
Con la tabla delante, cada fila es una animación completa e independiente. filter en este ejemplo no arranca en el 0%: arranca con un keyframe implícito en el 0% tomado del valor base, y su primer tramo real va de ahí al 30%. Si el elemento no tiene filter declarado, ese implícito es none, y none interpola de forma discreta contra la mayoría de funciones de filtro, así que verás un salto en vez de una transición.
Ese salto es el síntoma que delata el problema. Cuando una propiedad de una animación cambia de golpe en vez de deslizarse, casi siempre es porque uno de sus extremos es un valor implícito que resulta ser un keyword no interpolable: none, auto, normal. La solución no es cambiar la curva, es declarar explícitamente un valor interpolable en ese extremo.
Escribe una regla con tres keyframes donde opacity solo aparezca en el 0% y en el 100% con linear, y transform aparezca en los tres con un animation-timing-function: steps(4) en el 0%. Comprueba que la opacidad sube de forma completamente lisa mientras el transform avanza a saltos. Eso demuestra que las dos propiedades no comparten ni tramos ni curva.