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,rotateyscale - Definir animaciones con
@keyframesy controlar todas las subpropiedades deanimation - 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
transitionen 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.3ses cómodo, pero anima cualquier propiedad que cambie (incluidas algunas que no esperabas y otras costosas de calcular, comowidthopadding). 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, comolinear(0, 0.25, 1)), que permite aproximar curvas complejas como rebotes reales. Es más avanzada; conease-outy un par decubic-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
displayanimaopacity(y, si quieres que no reciba clics mientras está oculto, añadevisibility: hidden, que sí participa en las transiciones cambiando al final). Los navegadores actuales también permiten animardisplaycontransition-behavior: allow-discretey@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
transformyopacity. Para mover algo, usatransform: translate()en lugar de cambiarleftomargin-left; para agrandarlo,scaleen lugar dewidth.
/* ✗ 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-changea 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 esotranslate(-50%, -50%)desplaza un elemento la mitad de su ancho y alto: es la base del clásico centrado conposition: 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
:hoverescribestransform: scale(1.1)en un elemento que ya teníatransform: rotate(5deg), pierdes la rotación: la nueva declaración sustituye a la anterior entera. Tendrías que escribirtransform: 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
scaleen:hoverno borra elrotateque 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ésrotate, despuésscaley, por último, lo que haya entransform. Si necesitas otro orden, usatransformcon 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
:hoverreal. 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. Conanimation-fill-mode: both(obackwards), permanece invisible durante el retardo. Por eso en las animaciones de entrada casi siempre se usaboth.
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 usaanimation-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-timelinedespués de la abreviadaanimation, 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: pauseddesde 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-motiondel 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 |