wandres.dev
KEYFRAMES · @keyframes a fondo

Anatomía de una regla @keyframes

La regla de keyframes no es una lista de fotogramas: es una función a trozos definida por puntos de control. Selectores múltiples, orden irrelevante, y los keyframes que el navegador se inventa.

⏱ 18 min

Casi todo el mundo lee @keyframes como una tira de fotogramas: el 0%, el 50%, el 100%. Ese modelo mental se rompe en cuanto una propiedad falta en un keyframe intermedio o en cuanto el 0% ni siquiera está escrito. La regla no describe fotogramas; describe puntos de control de una función a trozos, uno por propiedad, y el navegador interpola entre los puntos que existan para cada una. Entender esa diferencia te ahorra la mitad de los bugs de animación que parecen inexplicables.

🎯 Al terminar esta lección sabrás
  • Escribir selectores de keyframe con porcentajes múltiples en una misma regla.
  • Predecir qué gana cuando dos bloques declaran el mismo offset.
  • Explicar de dónde salen los keyframes implícitos del 0% y del 100%.
  • Razonar el coste real de añadir keyframes a una animación.

La regla, sus selectores y por qué el orden da igual

Una regla @keyframes tiene un nombre y un cuerpo con bloques. Cada bloque lleva un selector de keyframe: from, to, o uno o varios porcentajes separados por comas. from es exactamente sinónimo de 0% y to de 100%; no hay ninguna diferencia semántica, solo de legibilidad.

@keyframes latido {
  from, to  { transform: scale(1); }
  20%, 60%  { transform: scale(1.18); }
  40%, 80%  { transform: scale(1); }
}

Lo interesante es que un mismo bloque puede llevar varios offsets. 20%, 60% no significa “de 20% a 60%”: significa que en el instante 20% y en el instante 60% la propiedad vale lo mismo. Es la forma compacta de escribir una animación con repeticiones internas sin duplicar declaraciones. Un latido doble, un rebote, un shake: todos se escriben así y con la mitad de líneas.

El orden en que escribes los bloques dentro del cuerpo no importa. El navegador los ordena por offset antes de nada. Puedes escribir el to primero y el from al final y el resultado es idéntico. Esto sorprende a quien viene de herramientas de animación con línea de tiempo, donde el orden es la estructura; aquí la estructura es el número, no la posición en el fichero.

Los offsets fuera del rango de 0% a 100% son inválidos en un selector de keyframe y hacen que ese bloque se descarte entero, en silencio, sin invalidar el resto de la regla. Si escribes 110% esperando un sobrepaso, no obtienes un error: obtienes una animación a la que le falta un tramo. El sobrepaso se consigue con la curva de easing, no con el offset.

Dos bloques con el mismo offset

Nada te impide declarar el mismo offset dos veces. Cuando eso pasa, no se sustituye el bloque entero: se resuelve propiedad a propiedad, y gana la declaración que aparece más tarde en el cuerpo de la regla.

@keyframes mixto {
  0%  { opacity: 0; transform: translateY(20px); }
  0%  { opacity: 0.2; }              /* gana este opacity */
  100% { opacity: 1; transform: none; }
}

El keyframe del 0% resultante es opacity: 0.2 más transform: translateY(20px). La segunda declaración de 0% no borra el transform de la primera; solo pisa la propiedad que ella misma declara. Es la cascada normal de CSS, aplicada dentro del ámbito de un único offset.

Esto tiene un uso legítimo: componer una animación base y luego sobrescribir un aspecto concreto sin reescribir el bloque. Pero es también una fuente de confusión cuando alguien duplica un offset por accidente y solo se pierde una propiedad. Si una animación se comporta raro en un instante puntual, busca offsets repetidos antes que cualquier otra cosa.

Los keyframes que no escribes

Aquí está el mecanismo que más gente desconoce. Si tu regla no declara el 0% o no declara el 100% para una propiedad concreta, el navegador sintetiza ese keyframe usando el valor computado que esa propiedad tiene en el elemento sin contar esta animación. Se llaman keyframes implícitos, y son lo que hace que esta regla funcione:

@keyframes shake {
  25% { transform: translateX(-8px); }
  75% { transform: translateX(8px); }
}

.error {
  transform: translateX(0);
  animation: shake 400ms ease-in-out 3;
}

No hay ni 0% ni 100%, y sin embargo el elemento sale de translateX(0), va a -8px, cruza hasta 8px y vuelve a 0. Los extremos los pone el navegador leyendo el transform que el elemento tendría de todos modos. Escribir una animación de “sacudida” que no impone un punto de partida la hace reutilizable: sirve para cualquier elemento, tenga el transform base que tenga.

El coste de esto es que la animación pasa a depender del estado del elemento. Si .error está dentro de un contenedor que le cambia el transform base al pasar el ratón, los extremos de la sacudida cambian con él. Es una función de lo que hay, no un valor absoluto, y eso es exactamente lo que la hace útil o lo que la vuelve impredecible según el caso.

Nivel dios

El keyframe implícito no se congela al arrancar la animación: es un valor vivo. El navegador lo recalcula si el valor base subyacente cambia mientras la animación corre, y la interpolación salta al nuevo destino en ese mismo frame. Si añades una clase que modifica el transform base a mitad de una sacudida sin extremos explícitos, verás un salto que no viene de ningún keyframe escrito. Cuando necesites que una animación sea inmune al estado base, declara el 0% y el 100% explícitamente aunque parezca redundante: es la única forma de convertir una función del entorno en una constante.

Cuántos keyframes son demasiados

La respuesta práctica: los que necesites. Un keyframe no es un frame de vídeo. El navegador no dibuja un fotograma por keyframe; interpola de forma continua entre el par de puntos que rodea al instante actual, y produce tantos frames como pueda la pantalla. Una animación con dos keyframes y otra con veinte cuestan lo mismo por frame: en ambos casos el trabajo es localizar el tramo activo y hacer una interpolación.

Lo que sí cuesta es qué propiedad animas y cuántos elementos la animan. Cincuenta keyframes de transform sobre un elemento son gratis comparados con dos keyframes de width sobre veinte elementos, porque el segundo caso dispara layout en cada frame. El número de puntos de control es la dimensión que menos importa del coste.

Donde el número sí pesa es en el mantenimiento. Una regla con dieciocho porcentajes es ilegible y casi siempre significa que estás aproximando a mano una curva que deberías expresar con easing. Si tus keyframes están describiendo la forma del movimiento en vez de sus hitos, el problema es de curva, y se resuelve en la función de temporización, no en la regla.

⚔️ Reto práctico

Escribe una regla @keyframes llamada pop que solo declare el 50% con transform: scale(1.25). Aplícala a dos botones con transform base distinto (uno sin transform, otro con rotate(3deg)) y comprueba en el inspector que cada uno vuelve a su propio estado base. Después añade un 0% y un 100% explícitos con scale(1) y observa cómo el botón rotado pierde su rotación durante la animación.