Módulo 13: Transiciones y animaciones

Objetivos del módulo

  • Saber cuándo y para qué animar una interfaz
  • Crear transiciones suaves entre estados con transition
  • Elegir la curva de aceleración adecuada, incluida cubic-bezier()
  • Conocer qué propiedades se pueden animar y cuáles son eficientes
  • Transformar elementos en 2D y 3D con transform
  • Usar las propiedades individuales translate, rotate y scale
  • Definir animaciones con @keyframes y controlar todas las subpropiedades de animation
  • Construir efectos habituales: cargadores, pulsos, apariciones y botones animados
  • Dar los primeros pasos con las animaciones ligadas al scroll
  • Respetar a los usuarios que prefieren menos movimiento

1. ¿Por qué animar?

Una animación bien usada no es decoración: comunica. Ayuda al usuario a entender qué ha pasado, de dónde viene un elemento o qué puede pulsar.

Propósito Ejemplo
Respuesta (feedback) Un botón que cambia de color suavemente al pasar el ratón
Orientación Un menú lateral que entra deslizándose desde la izquierda: sabes de dónde sale y adónde vuelve
Estado Un cargador girando mientras llegan los datos
Atención Una notificación que “late” una vez para que la veas
Continuidad Una tarjeta que se expande hasta convertirse en el detalle

📘 Concepto: Piensa en las puertas de un ascensor. Si desaparecieran de golpe, te sobresaltarías; como se deslizan, entiendes al instante qué ocurre. Las transiciones de una interfaz cumplen la misma función: hacen que los cambios sean comprensibles.

💡 Consejo: Las animaciones de interfaz deben ser rápidas: entre 150 y 300 ms para cambios pequeños (hover, foco) y hasta unos 500 ms para elementos grandes que entran o salen. Una animación de un segundo en un botón hace que la web parezca lenta.

CSS ofrece dos mecanismos:

  transition animation + @keyframes
Se dispara Al cambiar una propiedad (hover, clase nueva…) Sola, al cargar o al aplicar la clase
Estados Dos: inicial y final Tantos como quieras (0 %, 30 %, 100 %…)
Repetición No Sí (incluso infinita)
Uso típico Hover, foco, abrir/cerrar Cargadores, efectos de entrada, bucles

2. Transiciones: transition

Sin transición, un cambio de estado es instantáneo. Con ella, el navegador calcula los valores intermedios.

.boton {
    background-color: #2563eb;
    color: white;
    transition: background-color 0.3s ease;   /* ← en el estado BASE */
}

.boton:hover {
    background-color: #1e40af;
}

⚠️ Importante: Declara transition en el estado base (.boton), no en :hover. Así la transición funciona en los dos sentidos: al entrar y al salir. Si la pones solo en :hover, al quitar el ratón el cambio será brusco.

Las cuatro subpropiedades

Subpropiedad Qué indica Ejemplo
transition-property Qué propiedad se anima background-color, transform, all
transition-duration Cuánto dura 0.3s, 200ms
transition-timing-function Cómo se reparte la velocidad ease, linear, cubic-bezier(...)
transition-delay Cuánto espera antes de empezar 0s, 100ms
/* Forma larga */
.tarjeta {
    transition-property: transform;
    transition-duration: 250ms;
    transition-timing-function: ease-out;
    transition-delay: 0s;
}

/* Abreviada: propiedad duración curva retardo */
.tarjeta {
    transition: transform 250ms ease-out 0s;
}

💡 Consejo: En la abreviada, el primer tiempo es siempre la duración y el segundo, el retardo.

Varias propiedades con tiempos distintos

.tarjeta {
    transition:
        transform 250ms ease-out,
        box-shadow 250ms ease-out,
        background-color 150ms linear;
}

.tarjeta:hover {
    transform: translateY(-6px);
    box-shadow: 0 12px 24px rgb(0 0 0 / 0.15);
    background-color: #f8fafc;
}

⚠️ Importante: transition: all 0.3s es cómodo, pero anima cualquier propiedad que cambie (incluidas algunas que no esperabas y otras costosas de calcular, como width o padding). Enumera las propiedades que quieres animar.

Transiciones disparadas por clases

Las transiciones reaccionan a cualquier cambio de valor, no solo a :hover. Lo habitual es añadir o quitar una clase con JavaScript:

.panel {
    opacity: 0;
    translate: 0 20px;
    transition: opacity 300ms ease, translate 300ms ease;
}

.panel.visible {
    opacity: 1;
    translate: 0 0;
}
// Al pulsar el botón, el panel aparece suavemente
document.querySelector('#mostrar').addEventListener('click', () => {
    document.querySelector('.panel').classList.toggle('visible');
});

3. Curvas de aceleración (timing functions)

La curva decide cómo se reparte la velocidad a lo largo de la animación. En el mundo real nada arranca ni frena de golpe: una curva adecuada hace que el movimiento parezca natural.

Valor Comportamiento Equivalencia Uso recomendado
ease Arranca rápido y frena suavemente (por defecto) cubic-bezier(0.25, 0.1, 0.25, 1) Uso general
linear Velocidad constante cubic-bezier(0, 0, 1, 1) Giros continuos, barras de progreso
ease-in Empieza lento y acelera cubic-bezier(0.42, 0, 1, 1) Elementos que salen
ease-out Empieza rápido y frena cubic-bezier(0, 0, 0.58, 1) Elementos que entran, hover
ease-in-out Lento al principio y al final cubic-bezier(0.42, 0, 0.58, 1) Movimientos de ida y vuelta
steps(n) Salta en n pasos, sin intermedios — Efecto máquina de escribir, sprites

cubic-bezier(): curvas a medida

Una curva de Bézier se define con dos puntos de control (x1, y1, x2, y2). Las x (tiempo) deben estar entre 0 y 1; las y (progreso) pueden salirse de ese rango, lo que crea efectos de rebote o anticipación:

/* Salida enérgica, muy usada en interfaces modernas */
.menu  { transition: translate 400ms cubic-bezier(0.16, 1, 0.3, 1); }

/* Se pasa un poco del destino y vuelve (efecto "muelle") */
.globo { transition: scale 500ms cubic-bezier(0.34, 1.56, 0.64, 1); }

💡 Consejo: No intentes adivinar los números. En las DevTools de Chrome y Firefox, junto a cualquier curva aparece un pequeño icono que abre un editor visual de Bézier: arrastras los puntos y ves la animación en tiempo real. Webs como easings.net recopilan curvas listas para copiar.

📘 Concepto: Existe también la función linear() (con paradas, como linear(0, 0.25, 1)), que permite aproximar curvas complejas como rebotes reales. Es más avanzada; con ease-out y un par de cubic-bezier() tendrás cubierto el 95 % de los casos.


4. ¿Qué se puede animar? Rendimiento

Propiedades animables

Se pueden animar las propiedades cuyo valor tiene puntos intermedios calculables: números, longitudes, porcentajes, colores, sombras, transformaciones…

Se anima de forma suave No se anima (cambia de golpe)
opacity, color, background-color display
transform, translate, rotate, scale font-family
width, height, margin, padding position
box-shadow, border-radius, filter background-image (salvo casos concretos)
Variables CSS registradas con @property height: auto (de forma tradicional)

💡 Consejo: Para hacer aparecer y desaparecer algo suavemente, en lugar de display anima opacity (y, si quieres que no reciba clics mientras está oculto, añade visibility: hidden, que sí participa en las transiciones cambiando al final). Los navegadores actuales también permiten animar display con transition-behavior: allow-discrete y @starting-style, una técnica más avanzada.

Cómo pinta el navegador

Cada vez que algo cambia, el navegador puede tener que repetir hasta tres fases:

flowchart LR
    A["1. Layout<br/>calcular tamaños y posiciones"] --> B["2. Paint<br/>pintar píxeles"] --> C["3. Composite<br/>combinar capas"]
Si animas… Fases que se repiten Coste
width, height, margin, top, left, font-size Layout + Paint + Composite Alto: afecta también a los vecinos
color, background-color, box-shadow Paint + Composite Medio
transform, opacity Solo Composite Bajo: lo hace la GPU

⚠️ Importante: Para animaciones fluidas a 60 fotogramas por segundo, anima siempre que puedas transform y opacity. Para mover algo, usa transform: translate() en lugar de cambiar left o margin-left; para agrandarlo, scale en lugar de width.

/* ✗ Costoso: recalcula el layout de la página en cada fotograma */
.caja:hover { left: 100px; width: 300px; }

/* ✓ Eficiente: solo composición */
.caja:hover { transform: translateX(100px) scale(1.2); }

will-change

will-change avisa al navegador de que una propiedad va a animarse, para que prepare una capa propia por adelantado:

.menu-lateral {
    will-change: transform;
}

⚠️ Importante: Úsalo con moderación, solo en elementos que realmente se animan y cuando detectes un problema de rendimiento. Cada capa consume memoria de vídeo; aplicar will-change a muchos elementos (o * { will-change: transform; }) empeora el rendimiento en vez de mejorarlo.


5. Transformaciones 2D: transform

transform modifica la representación visual de un elemento sin afectar al flujo: los demás elementos se comportan como si siguiera en su sitio original.

Función Efecto Ejemplo
translate(x, y) Desplaza translate(20px, -10px), translateX(50%), translateY(-4px)
rotate(ángulo) Gira (positivo = sentido horario) rotate(45deg), rotate(0.5turn)
scale(x, y) Escala (1 = tamaño original) scale(1.1), scaleX(-1) (espejo)
skew(x, y) Inclina skew(10deg), skewY(-5deg)
.subir    { transform: translateY(-8px); }
.girar    { transform: rotate(15deg); }
.agrandar { transform: scale(1.05); }
.espejo   { transform: scaleX(-1); }
.inclinar { transform: skewX(-12deg); }

💡 Consejo: Los porcentajes en translate() son relativos al propio elemento, no al padre. Por eso translate(-50%, -50%) desplaza un elemento la mitad de su ancho y alto: es la base del clásico centrado con position: absolute; top: 50%; left: 50%;.

Combinar transformaciones: el orden importa

Puedes encadenar varias funciones separadas por espacios. Se aplican de derecha a izquierda (o, visto de otro modo, cada una transforma el sistema de coordenadas de las siguientes), así que el orden cambia el resultado:

/* Primero gira y luego se desplaza en la dirección girada: acaba en diagonal */
.a { transform: rotate(45deg) translateX(100px); }

/* Primero se desplaza a la derecha y después gira sobre sí mismo */
.b { transform: translateX(100px) rotate(45deg); }

⚠️ Importante: Si en :hover escribes transform: scale(1.1) en un elemento que ya tenía transform: rotate(5deg), pierdes la rotación: la nueva declaración sustituye a la anterior entera. Tendrías que escribir transform: rotate(5deg) scale(1.1). Las propiedades individuales de la sección 6 resuelven este problema.

transform-origin

Por defecto, las transformaciones se aplican respecto al centro del elemento. transform-origin cambia ese punto de anclaje:

.aguja {
    transform-origin: bottom center;   /* gira desde abajo, como la aguja de un reloj */
    transform: rotate(30deg);
}

.puerta {
    transform-origin: left;            /* gira sobre la bisagra izquierda */
}

.subrayado {
    transform-origin: left;            /* crece desde la izquierda */
    transform: scaleX(0);
    transition: transform 300ms ease-out;
}
a:hover .subrayado { transform: scaleX(1); }

6. Propiedades individuales: translate, rotate y scale

Los navegadores actuales permiten escribir cada transformación como una propiedad independiente:

.icono {
    translate: 0 -4px;     /* equivale a transform: translate(0, -4px) — sin comas */
    rotate: 15deg;
    scale: 1.1;
}

Ventajas:

  • Se combinan sin pisarse: cambiar scale en :hover no borra el rotate que ya tenía el elemento.
  • Se animan por separado, con duraciones y curvas distintas.
  • El código es más legible.
.carta {
    rotate: -3deg;                               /* inclinación permanente */
    transition: scale 200ms ease-out, rotate 400ms ease;
}

.carta:hover {
    scale: 1.08;                                 /* el rotate se mantiene... */
    rotate: 0deg;                                /* ...o lo cambiamos con su propio tiempo */
}

📘 Concepto: Las propiedades individuales se aplican siempre en este orden fijo: primero translate, después rotate, después scale y, por último, lo que haya en transform. Si necesitas otro orden, usa transform con las funciones en el orden que quieras.


7. Transformaciones 3D

Las transformaciones 3D añaden el eje Z (hacia el usuario y hacia el fondo de la pantalla).

Función Efecto
rotateX(ángulo) Gira alrededor del eje horizontal (como una persiana)
rotateY(ángulo) Gira alrededor del eje vertical (como una puerta)
rotateZ(ángulo) Gira en el plano de la pantalla (igual que rotate)
translateZ(distancia) Acerca (positivo) o aleja (negativo)
perspective(distancia) Perspectiva aplicada solo a ese elemento

perspective: la profundidad

Sin perspectiva, un giro 3D se ve “plano” (solo parece que el elemento se estrecha). perspective define a qué distancia está el observador: valores pequeños (200-400px) dan un efecto exagerado, como mirar de muy cerca; valores grandes (1000-2000px), un efecto sutil.

/* Opción 1: en el PADRE — todos los hijos comparten el mismo punto de fuga (lo habitual) */
.escena { perspective: 800px; }
.escena .panel:hover { transform: rotateY(25deg); }

/* Opción 2: como función, solo para ese elemento */
.panel-suelto:hover { transform: perspective(800px) rotateY(25deg); }

Proyecto: tarjeta que gira

Una tarjeta con dos caras que se da la vuelta al pasar el ratón (o al recibir el foco con teclado):

<div class="flip" tabindex="0">
    <div class="flip__interior">
        <div class="flip__cara flip__cara--frontal">
            <h3>Laura Martín</h3>
            <p>Desarrolladora front-end</p>
        </div>
        <div class="flip__cara flip__cara--trasera">
            <p>laura@ejemplo.com</p>
            <p>+34 600 000 000</p>
        </div>
    </div>
</div>
/* 1. La escena: define la perspectiva y el tamaño */
.flip {
    width: 280px;
    height: 180px;
    perspective: 1000px;
}

/* 2. El elemento que gira: conserva el 3D de sus hijos */
.flip__interior {
    position: relative;
    width: 100%;
    height: 100%;
    transform-style: preserve-3d;          /* ¡imprescindible! sin esto, los hijos se aplanan */
    transition: transform 0.6s cubic-bezier(0.4, 0.2, 0.2, 1);
}

.flip:hover .flip__interior,
.flip:focus-within .flip__interior,
.flip:focus .flip__interior {
    transform: rotateY(180deg);
}

/* 3. Las dos caras, superpuestas */
.flip__cara {
    position: absolute;
    inset: 0;
    display: grid;
    place-content: center;
    text-align: center;
    border-radius: 12px;
    backface-visibility: hidden;           /* la cara de atrás no se ve cuando está de espaldas */
    box-shadow: 0 8px 20px rgb(0 0 0 / 0.15);
}

.flip__cara--frontal {
    background: linear-gradient(135deg, #2563eb, #7c3aed);
    color: white;
}

/* 4. La trasera empieza ya girada 180°: al girar el conjunto, queda de frente */
.flip__cara--trasera {
    background-color: white;
    transform: rotateY(180deg);
}
Propiedad Papel en el efecto
perspective (en el padre) Da profundidad al giro
transform-style: preserve-3d Mantiene a los hijos en el espacio 3D del padre
backface-visibility: hidden Oculta cada cara cuando está de espaldas
rotateY(180deg) en la trasera La coloca “detrás”, mirando hacia el otro lado

⚠️ Importante: En pantallas táctiles no hay :hover real. Para un uso serio, añade una clase con JavaScript al pulsar la tarjeta (.flip.girada .flip__interior { ... }) o envuelve la tarjeta en un <button> para que sea accesible.


8. Animaciones: @keyframes y animation

8.1. Definir los fotogramas clave

@keyframes define qué ocurre en cada momento de la animación, con un nombre:

/* Con from y to (equivalen a 0 % y 100 %) */
@keyframes aparecer {
    from { opacity: 0; }
    to   { opacity: 1; }
}

/* Con porcentajes: tantos pasos como quieras */
@keyframes rebotar {
    0%, 100% { translate: 0 0; }        /* varios porcentajes comparten estilos */
    40%      { translate: 0 -30px; }
    60%      { translate: 0 -15px; }
}

8.2. Aplicarla: animation

.titulo {
    animation-name: aparecer;
    animation-duration: 1s;
}
Subpropiedad Qué controla Valores
animation-name Qué @keyframes usar El nombre definido
animation-duration Duración de un ciclo 1s, 300ms
animation-timing-function Curva de aceleración (se aplica entre cada par de fotogramas) ease, linear, steps(), cubic-bezier()
animation-delay Retardo antes de empezar (admite negativos: empieza “a mitad”) 0.5s, -1s
animation-iteration-count Número de repeticiones 1, 3, infinite
animation-direction Sentido de reproducción normal, reverse, alternate, alternate-reverse
animation-fill-mode Qué estilos aplicar antes de empezar y al terminar none, forwards, backwards, both
animation-play-state Pausar o reanudar running, paused

animation-fill-mode en detalle

Es la subpropiedad que más confunde. Por defecto, antes de empezar (durante el retardo) y después de terminar, el elemento vuelve a su estilo normal, como si la animación no existiera.

Valor Durante el retardo Al terminar
none Estilo normal Vuelve al estilo normal
forwards Estilo normal Se queda con el último fotograma
backwards Aplica ya el primer fotograma Vuelve al estilo normal
both Aplica el primer fotograma Se queda con el último

💻 Ejemplo: Un título con animation: aparecer 1s 0.5s; (con medio segundo de retardo) se ve visible durante ese medio segundo, después desaparece de golpe y entonces empieza a aparecer: un parpadeo feo. Con animation-fill-mode: both (o backwards), permanece invisible durante el retardo. Por eso en las animaciones de entrada casi siempre se usa both.

animation-direction con alternate

@keyframes respirar {
    from { scale: 1; }
    to   { scale: 1.15; }
}

.icono {
    /* ida y vuelta infinita: 1 → 1.15 → 1 → 1.15... sin saltos bruscos */
    animation: respirar 1.5s ease-in-out infinite alternate;
}

La abreviada

/* animation: nombre duración curva retardo repeticiones dirección relleno estado */
.elemento {
    animation: rebotar 1s ease-in-out 0.2s infinite normal both running;
}

/* Lo habitual es escribir solo lo necesario */
.elemento { animation: rebotar 1s ease-in-out infinite; }

/* Varias animaciones a la vez, separadas por comas */
.elemento { animation: aparecer 0.6s ease-out both, rebotar 2s 0.6s ease-in-out infinite; }

💡 Consejo: Igual que en transition, el primer tiempo es la duración y el segundo el retardo. El orden del resto de valores es flexible, pero acostúmbrate a empezar siempre por el nombre y la duración.

Pausar una animación

.carrusel-logos {
    animation: desplazar 20s linear infinite;
}

.carrusel-logos:hover {
    animation-play-state: paused;   /* se detiene al pasar el ratón, para poder leer */
}

9. Ejemplos prácticos

9.1. Cargador giratorio (spinner)

<div class="spinner" role="status" aria-label="Cargando"></div>
.spinner {
    width: 48px;
    aspect-ratio: 1;
    border: 5px solid #e5e7eb;
    border-top-color: #2563eb;      /* solo un lado coloreado */
    border-radius: 50%;
    animation: girar 0.8s linear infinite;
}

@keyframes girar {
    to { rotate: 1turn; }           /* "from" se omite: parte del estado actual */
}

9.2. Tres puntos que rebotan

<div class="puntos" role="status" aria-label="Escribiendo">
    <span></span><span></span><span></span>
</div>
.puntos {
    display: flex;
    gap: 6px;
}

.puntos span {
    width: 10px;
    aspect-ratio: 1;
    border-radius: 50%;
    background-color: #64748b;
    animation: saltar 0.9s ease-in-out infinite;
}

/* El retardo escalonado crea el efecto de "ola" */
.puntos span:nth-child(2) { animation-delay: 0.15s; }
.puntos span:nth-child(3) { animation-delay: 0.3s; }

@keyframes saltar {
    0%, 60%, 100% { translate: 0 0; opacity: 0.4; }
    30%           { translate: 0 -10px; opacity: 1; }
}

9.3. Pulso de notificación

.notificacion {
    position: relative;
    width: 14px;
    aspect-ratio: 1;
    border-radius: 50%;
    background-color: #dc2626;
}

/* Un anillo que se expande y se desvanece */
.notificacion::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: 50%;
    background-color: inherit;
    animation: pulso 1.5s ease-out infinite;
}

@keyframes pulso {
    from { scale: 1; opacity: 0.7; }
    to   { scale: 3; opacity: 0; }
}

9.4. Aparición escalonada al cargar

@keyframes entrar {
    from {
        opacity: 0;
        translate: 0 24px;
    }
    to {
        opacity: 1;
        translate: 0 0;
    }
}

.tarjeta {
    animation: entrar 0.6s cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* Cada tarjeta empieza un poco después que la anterior */
.tarjeta:nth-child(2) { animation-delay: 0.1s; }
.tarjeta:nth-child(3) { animation-delay: 0.2s; }
.tarjeta:nth-child(4) { animation-delay: 0.3s; }

💡 Consejo: Con variables CSS puedes escalonar sin escribir una regla por elemento: pon style="--i: 2" en cada tarjeta y usa animation-delay: calc(var(--i) * 100ms);.

9.5. Botón con destello

Un brillo diagonal que recorre el botón al pasar el ratón, hecho con un pseudo-elemento:

<a href="#" class="boton-brillo">Empieza gratis</a>
.boton-brillo {
    position: relative;
    display: inline-block;
    overflow: hidden;                      /* el brillo no se ve fuera del botón */
    padding: 0.9em 2em;
    border-radius: 999px;
    background-color: #7c3aed;
    color: white;
    text-decoration: none;
    font-weight: 600;
    transition: translate 200ms ease-out, box-shadow 200ms ease-out;
}

.boton-brillo::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(110deg, transparent 30%, rgb(255 255 255 / 0.45) 50%, transparent 70%);
    translate: -100% 0;                    /* empieza fuera, a la izquierda */
}

.boton-brillo:hover {
    translate: 0 -2px;
    box-shadow: 0 10px 20px rgb(124 58 237 / 0.35);
}

.boton-brillo:hover::after {
    animation: destello 0.8s ease-out;
}

@keyframes destello {
    to { translate: 100% 0; }              /* cruza hasta salir por la derecha */
}

.boton-brillo:active {
    translate: 0 0;
    scale: 0.97;                           /* "se hunde" al pulsar */
}

10. Animaciones ligadas al scroll (introducción)

Tradicionalmente, animar elementos según el desplazamiento de la página exigía JavaScript. Las animaciones dirigidas por el scroll (scroll-driven animations) lo hacen solo con CSS: en lugar de avanzar con el tiempo, la animación avanza con la posición del scroll.

⚠️ Importante: Soporte (octubre de 2026): disponibles en Chrome y Edge desde la versión 115 (2023) y en Safari desde la 26 (2025). En Firefox siguen detrás de una opción experimental. Úsalas como mejora progresiva: la página debe funcionar perfectamente sin ellas. Consulta caniuse.com antes de usarlas.

10.1. Barra de progreso de lectura: scroll()

<div class="progreso-lectura"></div>
@keyframes crecer {
    from { scale: 0 1; }
    to   { scale: 1 1; }
}

.progreso-lectura {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 5px;
    background-color: #2563eb;
    transform-origin: left;
    animation: crecer linear both;
    animation-timeline: scroll();   /* el progreso = cuánto has desplazado la página */
}

10.2. Elementos que aparecen al entrar en pantalla: view()

@keyframes revelar {
    from { opacity: 0; translate: 0 60px; }
    to   { opacity: 1; translate: 0 0; }
}

/* Solo si el navegador lo soporta y el usuario no ha pedido menos movimiento */
@supports (animation-timeline: view()) {
    @media (prefers-reduced-motion: no-preference) {
        .revelar {
            animation: revelar linear both;
            animation-timeline: view();          /* progreso = visibilidad del propio elemento */
            animation-range: entry 0% cover 40%; /* desde que asoma hasta que cubre el 40 % */
        }
    }
}
Propiedad / función Significado
animation-timeline: scroll() La línea de tiempo es el desplazamiento del contenedor con scroll (por defecto, el más cercano)
animation-timeline: view() La línea de tiempo es la visibilidad del elemento dentro de su contenedor con scroll
animation-range Qué tramo del recorrido ocupa la animación (entry, exit, cover, contain…)

⚠️ Importante: Declara animation-timeline después de la abreviada animation, porque esta la restablece a su valor por defecto. Y fíjate en que no indicamos duración: con una línea de tiempo de scroll, la “duración” la marca el recorrido.


11. Accesibilidad: prefers-reduced-motion

Para algunas personas (con trastornos vestibulares, migrañas o epilepsia), las animaciones grandes, los efectos parallax o los movimientos continuos provocan mareos, náuseas o crisis. Los sistemas operativos permiten activar “Reducir movimiento”, y CSS lo detecta.

Estrategia 1: reducir de forma global

Una red de seguridad que conviene incluir en todos los proyectos:

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

Se usa 0.01ms en lugar de 0 para que los eventos de JavaScript que esperan al final de una animación (animationend, transitionend) se sigan disparando.

Estrategia 2: movimiento solo si se acepta

Más cuidadosa: los movimientos grandes solo se activan cuando el usuario no ha pedido reducirlos.

.tarjeta {
    transition: opacity 200ms ease;       /* un fundido suave es aceptable para todos */
}

@media (prefers-reduced-motion: no-preference) {
    .tarjeta {
        transition: opacity 200ms ease, translate 300ms ease-out;
    }
    .tarjeta:hover {
        translate: 0 -6px;                /* el desplazamiento, solo para quien lo acepta */
    }
}

📘 Concepto: “Reducir movimiento” no significa “eliminar toda animación”. Los cambios de color u opacidad suaves suelen estar bien; lo que molesta son los desplazamientos grandes, los zooms, los giros, el parallax y los bucles infinitos.

Otras buenas prácticas

  • Nada de destellos: evita cualquier cosa que parpadee más de 3 veces por segundo (riesgo de crisis epilépticas fotosensibles).
  • Permite pausar cualquier animación que dure más de 5 segundos (carruseles, fondos animados), por ejemplo con animation-play-state: paused desde un botón.
  • No transmitas información solo con movimiento: el estado debe entenderse también sin animación.
  • Prueba tu web con la emulación de prefers-reduced-motion del panel Rendering de DevTools.

12. Ejercicios

Ejercicio 1: Botones con transición

Crea tres botones: uno que cambie de color de fondo, otro que suba 4px y aumente su sombra, y otro con un borde que se convierte en fondo relleno. Usa la transición en el estado base, enumera las propiedades (sin all) y prueba las curvas ease-out, linear y cubic-bezier(0.34, 1.56, 0.64, 1).

Ejercicio 2: Subrayado animado

Crea un menú de navegación en el que cada enlace muestre, al pasar el ratón o recibir el foco, un subrayado que crece de izquierda a derecha. Hazlo con un ::after con scale: 0 1 que pasa a scale: 1 1, y transform-origin: left. Después cambia el origen para que crezca desde el centro.

Ejercicio 3: Galería con zoom

Crea una galería de tarjetas con imagen en las que, al pasar el ratón, la imagen haga un zoom suave dentro de la tarjeta (scale: 1.1 con overflow: hidden en el contenedor) y aparezca desde abajo un pie de foto semitransparente. Comprueba con DevTools (panel Performance o Rendering → Paint flashing) que solo animas propiedades eficientes.

Ejercicio 4: Tarjeta que gira

Implementa la tarjeta de la sección 7 con tu información de contacto en la cara trasera. Añade una versión que gire en vertical (rotateX) y experimenta con valores de perspective de 300px, 800px y 2000px. Haz que también gire al recibir el foco con el teclado.

Ejercicio 5: Colección de cargadores

Crea tres indicadores de carga distintos usando solo CSS: un spinner de borde, tres puntos que rebotan con retardo escalonado y una barra que se llena y vacía con alternate. Añade a cada uno role="status" y una etiqueta accesible.

Ejercicio 6: Página con entradas animadas

Crea una página con un título, un subtítulo, un botón y una rejilla de 6 tarjetas que aparezcan de forma escalonada al cargar, usando una única @keyframes, animation-fill-mode: both y una variable --i para el retardo. Añade un botón con el efecto de destello de la sección 9.5.

Ejercicio 7: Scroll y accesibilidad

Añade a la página anterior una barra de progreso de lectura con animation-timeline: scroll() y secciones que se revelen con view(), envueltas en @supports. Después implementa las dos estrategias de prefers-reduced-motion y compruébalas con la emulación de DevTools. Verifica en Firefox (o en un navegador sin soporte) que el contenido sigue siendo visible.


Resumen

Concepto Sintaxis / idea clave
Transición transition: propiedad duración curva retardo; en el estado base
Curvas ease, linear, ease-in, ease-out, ease-in-out, steps(), cubic-bezier()
Rendimiento Anima transform y opacity; will-change con moderación
Transform 2D translate(), rotate(), scale(), skew(); el orden importa
Punto de anclaje transform-origin: left top;
Propiedades individuales translate: 0 -4px; rotate: 15deg; scale: 1.1;
3D perspective en el padre, rotateY(), transform-style: preserve-3d, backface-visibility: hidden
Fotogramas clave @keyframes nombre { from {} 50% {} to {} }
Animación animation: nombre duración curva retardo repeticiones dirección relleno;
Repetir animation-iteration-count: infinite; + animation-direction: alternate;
Mantener estados animation-fill-mode: both;
Pausar animation-play-state: paused;
Ligadas al scroll animation-timeline: scroll() / view() + animation-range (mejora progresiva)
Accesibilidad @media (prefers-reduced-motion: reduce) / no-preference