wandres.dev
ONTOLOGÍA · El mapa de la animación web

Cuándo no animar y cómo escribir la versión sin movimiento

El criterio para descartar una animación, qué significa exactamente prefers-reduced-motion, y por qué la versión reducida se escribe primero.

⏱ 16 min

La parte de la animación web que nunca se enseña es la de decidir que no toca animar. No por prudencia estética, sino porque hay tres categorías de caso donde el movimiento produce daño medible: interfaces que se usan muchas veces al día, contenido que aparece mientras el usuario ya está leyendo, y usuarios con trastornos vestibulares para quienes ciertos movimientos causan náusea real. Esta lección cierra la ontología con el criterio de exclusión y con la forma correcta de escribir la variante sin movimiento, que no es esconderla al final de la hoja de estilos.

🎯 Al terminar esta lección sabrás
  • Aplicar los tres criterios de exclusión antes de escribir una animación.
  • Explicar qué expresa prefers-reduced-motion y qué no expresa.
  • Escribir una animación con la variante reducida como camino base y no como excepción.
  • Distinguir entre eliminar el movimiento y eliminar la información que el movimiento transmitía.

Los tres criterios de exclusión

Frecuencia de uso. Una animación se ve una vez y encanta; se ve doscientas veces al día y estorba. La cuenta que decide no es cuánto dura la animación, es cuánto tiempo acumulado impone a lo largo de una jornada de trabajo real. Un desplegable de 300 milisegundos en una herramienta interna que se abre trescientas veces al día son noventa segundos diarios de espera impuesta por decisión de diseño. Ese cálculo cambia la respuesta correcta: en herramientas de uso intensivo, el rango razonable baja a la mitad o menos, y algunas transiciones simplemente sobran.

Interrupción de la lectura. Cualquier movimiento en la periferia captura la atención de forma involuntaria; es un mecanismo del sistema visual que no se puede desactivar por voluntad. Un elemento que aparece animado mientras el usuario lee un párrafo le roba la línea. Esto excluye una categoría entera: las animaciones de entrada disparadas por scroll sobre contenido textual. El texto que aparece con un desvanecido al entrar en el viewport es exactamente el caso donde el movimiento no informa de nada —el usuario ya sabe que hay contenido, porque está bajando hacia él— y sí interfiere.

Daño vestibular. Existe una población para la que ciertos patrones de movimiento provocan mareo, náusea o migraña. No es una preferencia estética. Los patrones de riesgo están identificados y son concretos: desplazamientos grandes a lo largo del eje de la pantalla, especialmente si son rápidos; efectos de paralaje donde varias capas se mueven a velocidades distintas; escalados grandes que simulan acercamiento; y rotaciones o giros. Los patrones de bajo riesgo son igual de concretos: cambios de opacidad, cambios de color, movimientos pequeños y escalados pequeños.

La frontera entre “pequeño” y “grande” no está normativizada, pero la guía operativa que funciona es preguntarse si el movimiento cubre una fracción apreciable de la pantalla. Un botón que se desplaza dos píxeles no simula movimiento propio. Un panel que cruza la pantalla entera sí.

Qué expresa prefers-reduced-motion exactamente

La media query prefers-reduced-motion tiene dos valores, no-preference y reduce, y refleja un ajuste del sistema operativo. Hay tres cosas que conviene tener claras sobre ella y que se malinterpretan de forma sistemática.

No dice “sin animaciones”. El nombre del ajuste en los sistemas operativos es reducir movimiento, no eliminar transiciones. Lo que el usuario ha pedido es que no le muevas cosas por la pantalla. Un desvanecido de opacidad, un cambio de color o un indicador de carga siguen siendo apropiados bajo reduce, y suprimirlos empeora la interfaz sin beneficio para nadie. Sustituir todo movimiento por un corte seco es una respuesta perezosa.

No es una señal de gusto, es una señal de salud. Por eso el tratamiento correcto no es “degradar” sino “ofrecer una variante equivalente”. Si el movimiento comunicaba una relación —de dónde salió este panel, qué sustituyó a qué—, la variante reducida tiene que comunicar lo mismo por otro medio: un desvanecido, un cambio de posición sin trayectoria, un indicador explícito.

El valor por defecto es no-preference. Un usuario que nunca ha tocado el ajuste no aparece como reduce. Eso importa para el orden en que se escribe el CSS, y es donde casi todo el mundo lo hace al revés.

/* Mal: el movimiento es el camino base y la reduccion es un parche
   que hay que recordar escribir en cada componente. */
.panel { transition: translate 300ms ease-out; }
@media (prefers-reduced-motion: reduce) {
  .panel { transition: none; }
}
/* Bien: el camino base no mueve nada; el movimiento se anade
   cuando el usuario no ha pedido lo contrario. */
.panel {
  transition: opacity 160ms linear;
}

@media (prefers-reduced-motion: no-preference) {
  .panel {
    transition: opacity 160ms linear, translate 300ms ease-out;
  }
}

La diferencia no es estilística. En la primera versión, olvidarte de escribir el bloque de reducción produce una interfaz que daña a alguien. En la segunda, olvidarte produce una interfaz sin movimiento, que es exactamente el peor caso aceptable. La forma correcta de escribir código defensivo es que el olvido caiga del lado seguro.

Desde JavaScript el mismo criterio, con el mismo orden:

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

function abrir(panel) {
  const fotogramas = sinMovimiento.matches
    ? [{ opacity: 0 }, { opacity: 1 }]
    : [{ opacity: 0, translate: '0 12px' }, { opacity: 1, translate: '0 0' }];

  return panel.animate(fotogramas, {
    duration: sinMovimiento.matches ? 120 : 260,
    easing: 'ease-out',
    fill: 'forwards',
  }).finished;
}

Fíjate en que la variante reducida no dura cero: dura menos. Cero milisegundos es un corte, y un corte también puede desorientar, solo que de otra manera.

Reducir movimiento no es reducir duración, y confundirlo rompe tu propio código

Hay un fallo que aparece en casi todas las bases de código que “soportan” prefers-reduced-motion y que no se descubre hasta que algo se cuelga: poner transition-duration: 0s o animation-duration: 0.01ms de forma global bajo la media query. Se ve escrito así en decenas de reinicios copiados, y tiene dos consecuencias que nadie prevé. La primera es que los eventos siguen disparándose, pero el orden temporal cambia: si tu código espera transitionend o la promesa finished para revelar un elemento o para desbloquear un botón, con duración cero esos eventos se emiten en el mismo ciclo o directamente no se emiten, según la propiedad y el motor. Una interfaz que funcionaba pasa a quedarse a medias solo para los usuarios que han pedido menos movimiento, que es justo la población a la que menos te puedes permitir romperle nada. La segunda es más sutil: animation-duration: 0.01ms !important combinado con animation-iteration-count: 1 !important —el patrón del reinicio famoso— desactiva también los indicadores de carga, porque un spinner es una animación infinita, y deja al usuario mirando una rueda congelada sin saber si la aplicación está trabajando o se ha colgado. La regla que evita las dos cosas es no tocar nunca las duraciones de forma global: cambia qué se anima, no cuánto dura. Sustituye desplazamiento por opacidad, elimina el paralaje, quita el escalado grande, y deja intactas las animaciones que comunican estado del sistema. Y si de verdad necesitas una vía de escape global para una emergencia, que sea una que respete las animaciones infinitas y que no dependa de que los eventos se disparen.

Qué hacer con la información que el movimiento transmitía

Cuando quitas una animación, quitas dos cosas: el movimiento y lo que el movimiento decía. Lo primero es el objetivo; lo segundo es una pérdida que hay que compensar.

Si la animación indicaba origen —este menú salió de este botón—, la variante reducida puede indicarlo con posición: el menú aparece adosado al botón, sin trayectoria. La información se conserva en la geometría final.

Si indicaba continuidad —esta tarjeta es la misma que había en la lista—, se puede conservar con un elemento compartido evidente: el mismo título, la misma imagen, el mismo color de acento. La identidad se comunica por atributos visuales en vez de por trayectoria.

Si indicaba causalidad —esto ocurrió porque pulsaste aquello—, un cambio de opacidad corto en el destino basta para dirigir la mirada, y no implica movimiento.

Y si indicaba progreso —el sistema está trabajando—, no se elimina en absoluto: un indicador de carga es información de estado, no decoración. Aquí la variante reducida puede cambiar de forma, por ejemplo de una rueda que gira a una barra que crece o a un texto que cuenta, pero nunca desaparecer.

El ejercicio de escribir estas cuatro compensaciones tiene un efecto secundario que compensa el trabajo: te obliga a nombrar qué comunicaba cada animación. Las que no comunicaban nada se caen solas, y esa es la mejor auditoría de movimiento que existe.

⚔️ Invierte el orden en un componente real
  1. Coge un componente con transiciones y reescribe su CSS de forma que el camino base no mueva nada y el movimiento viva dentro de prefers-reduced-motion: no-preference.
  2. Fuerza la preferencia en tu sistema operativo o en el emulador de las herramientas de desarrollo y comprueba que la variante reducida sigue siendo comprensible sin explicaciones.
  3. Para cada animación que hayas conservado, escribe en una línea qué información transmite. Borra las que no tengan respuesta.