Saltar al contenido
animaciones.css · devschool

Animaciones CSS con @keyframes

Lección 35 de 50 · 12 min de lectura · Actualizado el

En esta lección
  1. Transición o animación
  2. Definir los fotogramas: @keyframes
  3. Aplicar la animación
  4. El atajo animation
  5. Tres ejemplos útiles
  6. Respetar prefers-reduced-motion
  7. Rendimiento: anima transform y opacity
  8. Novedad: animaciones ligadas al scroll
  9. Errores frecuentes
  10. Resumen

Con transition puedes animar el paso de un estado a otro cuando algo cambia, por ejemplo al pasar el ratón. Pero hay movimientos que no encajan ahí: un icono de carga que gira sin parar, un aviso que late para llamar la atención, una lista que aparece elemento a elemento al abrir la página. Para eso están las animaciones con fotogramas clave. En esta lección aprenderás a definirlas con @keyframes, a controlarlas con las propiedades animation-*, a respetar a quien prefiere menos movimiento y a que vayan fluidas.

Transición o animación

transitionanimation
Cuándo empiezaCuando cambia una propiedad (:hover, una clase nueva…)Sola, en cuanto se aplica
EstadosDos: el de antes y el de despuésTodos los fotogramas que quieras
RepeticiónNoSí, incluso infinita
Uso típicoBotones, menús, cambios de estadoCargadores, entradas, avisos

Si solo necesitas ir de A a B como respuesta a algo, una transición es más sencilla. Si necesitas más pasos, repetición o que empiece sola, usa una animación.

Definir los fotogramas: @keyframes

Un fotograma clave (keyframe) describe cómo debe estar el elemento en un momento concreto de la animación. El navegador calcula los estados intermedios. Se define con @keyframes y un nombre que tú eliges:

@keyframes aparecer {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: translateY(0); }
}

from es el inicio (equivale a 0%) y to, el final (100%). Para más pasos, usa porcentajes de la duración total:

@keyframes semaforo {
  0%   { background: #22c55e; }
  45%  { background: #22c55e; }   /* verde casi la mitad del tiempo */
  50%  { background: #f59e0b; }
  60%  { background: #ef4444; }
  100% { background: #ef4444; }
}

Varios porcentajes pueden compartir estilos, separados por comas:

@keyframes latido {
  0%, 50%, 100% { scale: 1; }
  25%, 75%      { scale: 1.2; }
}

Si omites from o to, el navegador usa los valores que el elemento ya tiene. Por eso basta con escribir el estado de inicio para una entrada:

@keyframes entrar-desde-abajo {
  from { opacity: 0; translate: 0 30px; }   /* termina en su estado normal */
}

Aplicar la animación

Los fotogramas solo son la receta. Para usarla, el elemento necesita al menos dos propiedades: el nombre y la duración.

.aviso {
  animation-name: aparecer;
  animation-duration: 0.4s;
}

Sin duración, la animación dura 0 segundos y no se ve nada. Vamos con el resto de propiedades.

animation-timing-function: la curva

Controla cómo se reparte la velocidad. Los valores son los mismos que en las transiciones:

  • ease (por defecto): empieza suave, acelera y frena al final.
  • linear: velocidad constante. Ideal para algo que gira sin parar.
  • ease-out: rápido al principio y frena. Natural para elementos que entran.
  • ease-in: lento al principio y acelera. Para elementos que salen.
  • ease-in-out: suave en los dos extremos. Para movimientos de ida y vuelta.
  • cubic-bezier(...): una curva a medida. Las DevTools tienen un editor visual para crearla.
  • steps(n): sin transición, a saltos, en n pasos. Sirve para relojes, efectos de máquina de escribir o animaciones de sprites.

La curva se aplica entre cada par de fotogramas, no a la animación entera.

animation-delay: el retraso

Cuánto espera antes de empezar. Con un retraso negativo, la animación empieza ya avanzada: animation-delay: -0.5s en una animación de 2s arranca por el 25 %.

Combinado con variables, permite que los elementos de una lista aparezcan uno tras otro (un efecto llamado escalonado):

.lista li {
  animation: aparecer 0.5s ease-out both;
  animation-delay: calc(var(--i) * 100ms);
}
<li style="--i: 0">Lunes</li>
<li style="--i: 1">Martes</li>
<li style="--i: 2">Miércoles</li>

animation-iteration-count: repeticiones

Un número (3, incluso 2.5) o infinite para que no pare nunca. Por defecto, 1.

animation-direction: el sentido

ValorQué hace
normalSiempre de 0 % a 100 % (por defecto)
reverseSiempre de 100 % a 0 %
alternateIda y vuelta: 0→100, 100→0, 0→100…
alternate-reverseIda y vuelta empezando por el final

alternate evita el salto brusco al volver al principio. Una caja que flota arriba y abajo:

@keyframes flotar {
  to { translate: 0 -10px; }
}
.globo { animation: flotar 1.5s ease-in-out infinite alternate; }

animation-fill-mode: antes y después

Por defecto, una animación solo afecta al elemento mientras se está ejecutando. Al terminar, el elemento vuelve de golpe a sus estilos normales. Y durante el retraso inicial, se ve con sus estilos normales, no con los del primer fotograma.

ValorQué hace
noneSin efecto fuera de la animación (por defecto)
forwardsAl terminar, se queda como en el último fotograma
backwardsDurante el retraso, ya se muestra como el primer fotograma
bothLas dos cosas

El ejemplo típico del problema: una lista escalonada con opacity: 0 en el from. Sin backwards, cada elemento se ve un instante, desaparece al empezar su animación y vuelve a aparecer. Con both (o backwards), espera invisible hasta su turno. Por eso las entradas suelen llevar both.

animation-play-state: pausar

running (por defecto) o paused. Permite detener una animación sin reiniciarla:

.carrusel { animation: desplazar 20s linear infinite; }
.carrusel:hover { animation-play-state: paused; }   /* se para al pasar el ratón */

El atajo animation

Todo en una línea:

.cargador { animation: girar 1s linear infinite; }
.aviso    { animation: aparecer 0.4s ease-out 0.2s both; }
/*                     nombre   duración curva retraso fill-mode */

El orden es bastante libre, con una regla: el primer tiempo es la duración y el segundo, el retraso. Pon nombres que no coincidan con palabras clave (infinite, ease, none…) para que el navegador no se confunda.

Puedes aplicar varias animaciones a la vez separándolas con comas:

.estrella {
  animation:
    girar 4s linear infinite,
    latido 1s ease-in-out infinite;
}

Si las dos animan transform, la segunda pisa a la primera. Usa propiedades distintas (rotate en una y scale en otra) para que se sumen.

Tres ejemplos útiles

Cargador que gira

.cargador {
  width: 32px;
  aspect-ratio: 1;
  border: 4px solid #dde3ee;
  border-top-color: #2d6cdf;   /* solo un lado de color: se ve el giro */
  border-radius: 50%;
  animation: girar 0.8s linear infinite;
}
@keyframes girar {
  to { rotate: 1turn; }
}

Ponle role="status" y un aria-label="Cargando" en el HTML para que los lectores de pantalla anuncien qué está pasando.

Pulso de notificación

@keyframes pulso {
  0%   { box-shadow: 0 0 0 0 rgb(230 57 70 / 0.6); }
  100% { box-shadow: 0 0 0 12px rgb(230 57 70 / 0); }
}
.punto-rojo { animation: pulso 1.5s ease-out infinite; }

Un anillo que crece y se desvanece alrededor del punto.

Esqueleto de carga

Los rectángulos grises con un brillo que avanza, mientras llegan los datos:

.esqueleto {
  height: 16px;
  border-radius: 4px;
  background: linear-gradient(90deg, #e5e7eb 30%, #f3f4f6 50%, #e5e7eb 70%);
  background-size: 300% 100%;
  animation: brillo 1.4s linear infinite;
}
@keyframes brillo {
  from { background-position: 100% 0; }
  to   { background-position: 0 0; }
}

El degradado es tres veces más ancho que la caja; al mover su posición, la franja clara cruza de lado a lado. Tienes más sobre esto en degradados.

Respetar prefers-reduced-motion

Algunas personas sufren mareos, náuseas o pérdida de concentración con el movimiento en pantalla. En su sistema operativo pueden activar la opción de reducir el movimiento, y el navegador lo comunica con la media query prefers-reduced-motion.

La forma más segura es aplicar las animaciones solo si la persona no ha pedido lo contrario:

@media (prefers-reduced-motion: no-preference) {
  .tarjeta { animation: aparecer 0.5s ease-out both; }
}

O, si ya tienes animaciones por todo el CSS, añadir una red de seguridad general al final:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Se usa 0.01ms y no none porque así la animación termina al instante pero sigue existiendo: el elemento llega a su estado final y el JavaScript que espera a que acabe la animación sigue funcionando.

Reducir no significa eliminar todo: un cargador puede seguir girando más despacio, porque indica que algo está pasando. Quita los desplazamientos grandes, los rebotes y el parallax. Tienes más en CSS accesible.

Rendimiento: anima transform y opacity

Para mostrar una página, el navegador calcula la posición de cada caja, la pinta y luego compone las capas. Si animas width, height, top, left o margin, tiene que recalcular posiciones y repintar en cada fotograma (60 veces por segundo), y en un móvil modesto la animación va a tirones.

transform (y las propiedades translate, rotate, scale) y opacity se resuelven en la última fase, a menudo con la tarjeta gráfica:

/* Mal: recalcula el diseño en cada fotograma */
@keyframes deslizar-mal { from { left: -300px; } to { left: 0; } }

/* Bien: solo se recompone */
@keyframes deslizar { from { translate: -100% 0; } to { translate: 0 0; } }

Existe la propiedad will-change: transform, que avisa al navegador de que algo se va a animar. Úsala solo si notas tirones y en pocos elementos: cada uno consume memoria de la tarjeta gráfica.

Novedad: animaciones ligadas al scroll

Normalmente, una animación avanza con el tiempo. Con animation-timeline puede avanzar con el scroll: al bajar por la página, la animación progresa; al subir, retrocede. Sin una línea de JavaScript.

Una barra de progreso de lectura en lo alto de la página:

.progreso-lectura {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  background: #2d6cdf;
  transform-origin: left;
  scale: 0 1;
}

@keyframes crecer {
  to { scale: 1 1; }
}

@supports (animation-timeline: scroll()) {
  .progreso-lectura {
    animation: crecer linear both;
    animation-timeline: scroll(root);   /* avanza con el scroll de la página */
  }
}

Y elementos que aparecen al entrar en pantalla, con view():

@supports (animation-timeline: view()) {
  .seccion {
    animation: aparecer linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 30%;  /* desde que asoma hasta que ocupa un 30 % */
  }
}
  • scroll(): el progreso depende de cuánto has bajado en un contenedor con scroll (root es la página).
  • view(): el progreso depende de la posición del propio elemento mientras cruza la pantalla.
  • animation-range: qué tramo de ese recorrido ocupa la animación.

Dos detalles: animation-timeline debe ir después del atajo animation, porque el atajo la reinicia; y la duración no se usa, la marca el scroll. Funciona en Chrome, Edge y Safari recientes; en Firefox aún no está activado por defecto. Por eso va dentro de @supports: sin soporte, el elemento se ve normal y no se pierde nada. Lo verás con más calma en la lección de scroll.

Errores frecuentes

  • No poner duración. animation: girar infinite no se ve: la duración por defecto es 0.
  • El elemento vuelve de golpe al terminar. Falta animation-fill-mode: forwards (o both).
  • Parpadeo antes de una entrada con retraso. Falta backwards o both: durante el retraso se ve el estado normal.
  • Escribir mal el nombre. Si el nombre de animation no coincide exactamente con el de @keyframes, no pasa nada y no hay error. Revisa mayúsculas y guiones.
  • Animar width, top o margin. Va a tirones. Anima transform y opacity.
  • Dos animaciones que pisan el mismo transform. Solo se ve una. Repártelas entre translate, rotate y scale.
  • Olvidar prefers-reduced-motion. Una animación infinita sin alternativa puede hacer inutilizable tu web para algunas personas.
  • Animaciones que parpadean rápido. Más de tres destellos por segundo pueden provocar crisis epilépticas. Evítalos siempre.

Resumen

PropiedadQué controlaValores habituales
@keyframes nombreLos fotogramas de la animaciónfrom, to, 0%…100%
animation-nameQué animación usarEl nombre de @keyframes
animation-durationCuánto dura una vuelta0.4s, 800ms
animation-timing-functionLa curva de velocidadease-out, linear, steps(4)
animation-delayEspera antes de empezar0.2s, negativo para empezar avanzada
animation-iteration-countRepeticiones1, 3, infinite
animation-directionSentidonormal, alternate
animation-fill-modeEstilos antes y despuésforwards, both
animation-play-statePausarrunning, paused
animationAtajo de todo lo anteriorgirar 1s linear infinite
prefers-reduced-motionRespetar a quien quiere menos movimientoreduce, no-preference
animation-timelineLigar la animación al scrollscroll(), view()

Ya sabes animar elementos. Ahora vamos a ver cómo controlar el desplazamiento de la página en sí: sigue con scroll.

Pruébalo tú

Cambia el código y pulsa Ejecutar (o Ctrl + Enter).

animaciones.html
Resultado

Pon a prueba lo que has aprendido

[CSS] Escribes animation: girar infinite; y el elemento no se mueve. ¿Cuál es la causa más probable?

[CSS] Una animación de entrada con delay hace parpadear el elemento visible antes de empezar. ¿Qué valor de animation-fill-mode lo arregla?

[CSS] ¿Qué propiedades conviene animar para que vaya fluido?

[CSS] ¿Qué hace animation-direction: alternate con animation-iteration-count: infinite?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.