Tabla de contenido

Las animaciones web hace unos años requerían JavaScript o Flash. Hoy, CSS tiene todo lo necesario para crear animaciones fluidas, performantes y accesibles. Desde efectos hover simples hasta loaders complejos, CSS lo maneja solo.

Transitions vs Animations

Antes de entrar en código, conviene entender la diferencia entre los dos mecanismos:

transition: anima el cambio de un estado a otro (de A a B). Necesita un detonador (hover, focus, clase que cambia). Es simple y directo.

@keyframes + animation: define una secuencia de estados (A → B → C → …). Puede correr automáticamente, en bucle, sin necesidad de interacción del usuario.

Regla general: usa transition para efectos de interacción, animation para movimientos continuos o secuencias complejas.

CSS Transitions

La propiedad transition anima suavemente el cambio de cualquier propiedad CSS:

.boton {
  background-color: #E53E3E;
  color: white;
  padding: 12px 24px;
  border-radius: 8px;
  
  /* Sintaxis: transition: propiedad duración timing-function delay */
  transition: background-color 0.3s ease;
}

.boton:hover {
  background-color: #c53030;
}

Puedes animar múltiples propiedades:

.tarjeta {
  transform: translateY(0);
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  
  transition: 
    transform 0.3s ease,
    box-shadow 0.3s ease;
}

.tarjeta:hover {
  transform: translateY(-8px);
  box-shadow: 0 12px 24px rgba(0,0,0,0.2);
}

O usar all para animar todas las propiedades que cambien (con precaución, puede ser costoso):

.elemento {
  transition: all 0.25s ease-in-out;
}

Funciones de timing comunes:

transition-timing-function: ease;         /* suave en ambos extremos (por defecto) */
transition-timing-function: ease-in;      /* lento al inicio, rápido al final */
transition-timing-function: ease-out;     /* rápido al inicio, lento al final */
transition-timing-function: ease-in-out;  /* lento en ambos extremos */
transition-timing-function: linear;       /* velocidad constante */
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* personalizado */

CSS Animations con @keyframes

@keyframes define la secuencia de la animación:

@keyframes nombre-animacion {
  from { /* estado inicial */ }
  to   { /* estado final */ }
}

/* O con porcentajes para múltiples pasos: */
@keyframes pulsar {
  0%   { transform: scale(1); }
  50%  { transform: scale(1.1); }
  100% { transform: scale(1); }
}

Luego aplicas la animación con la propiedad animation:

.icono {
  animation: pulsar 1.5s ease-in-out infinite;
}

Propiedades de animation

.elemento {
  animation-name: pulsar;           /* nombre del @keyframes */
  animation-duration: 1.5s;        /* duración de un ciclo */
  animation-timing-function: ease; /* curva de velocidad */
  animation-delay: 0.5s;           /* espera antes de iniciar */
  animation-iteration-count: 3;    /* repeticiones (infinite = infinito) */
  animation-direction: alternate;  /* normal | reverse | alternate | alternate-reverse */
  animation-fill-mode: forwards;   /* none | forwards | backwards | both */
  animation-play-state: running;   /* running | paused */
}

/* Shorthand: */
.elemento {
  animation: pulsar 1.5s ease 0.5s 3 alternate forwards;
}

animation-fill-mode: forwards hace que el elemento se quede en el estado final después de que la animación termina.

Ejemplos prácticos

Spinner/loader:

@keyframes girar {
  to { transform: rotate(360deg); }
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid rgba(229, 62, 62, 0.2);
  border-top-color: #E53E3E;
  border-radius: 50%;
  animation: girar 0.8s linear infinite;
}

Fade-in al cargar:

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

.hero-content {
  animation: fadeIn 0.6s ease-out;
}

Shake (efecto de error):

@keyframes shake {
  0%, 100% { transform: translateX(0); }
  10%, 30%, 50%, 70%, 90% { transform: translateX(-8px); }
  20%, 40%, 60%, 80% { transform: translateX(8px); }
}

.input-error {
  animation: shake 0.5s ease-in-out;
}

Texto parpadeante (cursor de escritura):

@keyframes parpadear {
  0%, 100% { opacity: 1; }
  50% { opacity: 0; }
}

.cursor {
  display: inline-block;
  width: 2px;
  height: 1.2em;
  background: #E53E3E;
  animation: parpadear 1s step-end infinite;
}

Skeleton loading (mientras carga contenido):

@keyframes skeleton {
  0% { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}

.skeleton {
  background: linear-gradient(
    90deg,
    #1A1B1D 25%,
    #222426 50%,
    #1A1B1D 75%
  );
  background-size: 200% 100%;
  animation: skeleton 1.5s infinite;
  border-radius: 8px;
}

Accesibilidad y prefers-reduced-motion

No todos los usuarios pueden tolerar animaciones. Las personas con epilepsia fotosensible, vértigo o ciertos trastornos neurológicos pueden sufrir con animaciones intensas.

El media query prefers-reduced-motion detecta si el usuario configuró en su sistema operativo que prefiere menos movimiento:

/* Animaciones por defecto */
.elemento {
  animation: fadeIn 0.6s ease-out;
  transition: transform 0.3s ease;
}

/* Para usuarios que prefieren menos movimiento */
@media (prefers-reduced-motion: reduce) {
  .elemento {
    animation: none;
    transition: none;
  }
  
  /* O una alternativa más sutil: */
  .spinner {
    animation-duration: 2s; /* más lento */
  }
}

Esto es una buena práctica en cualquier proyecto web profesional. Y en Astro o cualquier framework moderno, también puedes chequear esta preferencia con JavaScript:

const prefiereMenosMovimiento = window.matchMedia(
  '(prefers-reduced-motion: reduce)'
).matches;

Las animaciones CSS bien usadas hacen que tu interfaz se sienta viva y profesional. El truco está en la sutileza: las mejores animaciones son las que el usuario apenas nota conscientemente, pero que hacen la experiencia más fluida.