wandres.dev
MEDIA QUERIES MODERNAS · Rangos y consultas de usuario

prefers-reduced-motion: reducir no es eliminar

Qué ajuste del sistema activa la consulta, qué tipo de movimiento hace daño de verdad, por qué el reset universal es una mala primera opción y cómo escribir una alternativa que sigue comunicando.

⏱ 17 min

Hay personas para las que una animación de paralaje o un desplazamiento suave provocan mareo, náusea y desorientación durante horas. No es una cuestión de preferencia estética: los trastornos vestibulares son un problema médico y afectan a una fracción de la población lo bastante grande como para que todos los sistemas operativos tengan un interruptor para ello. prefers-reduced-motion es ese interruptor llegando a tu CSS, y atenderlo bien no consiste en apagar todas las animaciones.

🎯 Al terminar esta lección sabrás
  • Identificar qué ajuste del sistema operativo activa la consulta en cada plataforma.
  • Distinguir el movimiento que provoca síntomas vestibulares del que no.
  • Escribir alternativas reducidas que conserven la información que transmitía la animación.
  • Evaluar críticamente el reset universal y saber cuándo usarlo.

Qué activa la consulta

La consulta tiene dos valores, reduce y no-preference, y refleja un ajuste del sistema: en macOS e iOS es “Reducir movimiento” en accesibilidad, en Windows “Mostrar animaciones en Windows”, en Android “Quitar animaciones”, y en GNOME “Reducir animación”. El navegador no tiene ningún ajuste propio: lee el del sistema.

El soporte es universal desde hace años en los cuatro motores, así que puedes escribir las dos direcciones sin preocuparte:

/* dirección 1: por defecto hay movimiento, se retira si lo piden */
.panel { transition: transform 250ms ease; }
@media (prefers-reduced-motion: reduce) {
  .panel { transition: none; }
}

/* dirección 2: por defecto no hay movimiento, se añade si no hay preferencia */
@media (prefers-reduced-motion: no-preference) {
  .panel { transition: transform 250ms ease; }
}

La segunda dirección es preferible cuando la animación es puramente decorativa, porque el estado por defecto —sin movimiento— es el seguro y la animación es una adición. La primera es más práctica cuando la animación forma parte del diseño y solo quieres desactivar una parte.

Qué movimiento hace daño

No todo el movimiento es problemático, y tratar todo igual empobrece la interfaz sin beneficio. Lo que dispara síntomas vestibulares es el movimiento que simula desplazamiento en el espacio:

  • Desplazamientos grandes de elementos por la pantalla, sobre todo si son rápidos o si recorren mucha distancia.
  • Paralaje: capas moviéndose a velocidades distintas.
  • Escalados y zooms pronunciados.
  • Rotaciones y giros.
  • Desplazamiento automático o suave de gran recorrido.

Lo que en general no es problemático: cambios de opacidad, cambios de color, transiciones cortas de menos de unos 200ms sobre distancias pequeñas, y los indicadores de estado como un cursor de carga discreto.

Esa distinción da la regla operativa: sustituye traslación por opacidad. La animación de entrada que desplaza una tarjeta 40px hacia arriba se convierte en un fundido; la transición de página que empuja una vista se convierte en un cruce de opacidades. La información —“algo nuevo ha aparecido”— se conserva; el movimiento espacial desaparece.

.tarjeta { animation: entrar 400ms ease-out both; }

@keyframes entrar {
  from { opacity: 0; transform: translateY(2rem); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .tarjeta { animation: aparecer 200ms ease-out both; }
}

@keyframes aparecer {
  from { opacity: 0; }
  to   { opacity: 1; }
}

El reset universal y sus problemas

Circula un fragmento que desactiva todo de golpe:

@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;
  }
}

Está mejor pensado de lo que parece. La duración es 0.01ms en vez de 0 precisamente para que los eventos animationend y transitionend sigan disparándose: mucho código de aplicación espera esos eventos para retirar un elemento del DOM o para desbloquear un botón, y poner la duración a cero los cancela y deja la interfaz colgada. El animation-iteration-count: 1 corta los bucles infinitos, y scroll-behavior: auto desactiva el desplazamiento suave.

Sus problemas son dos y son reales. El primero es que el !important universal es imposible de anular: si un componente necesita conservar una animación —un indicador de progreso que comunica que el sistema sigue vivo— no hay forma limpia de exceptuarlo. El segundo es que empobrece la experiencia de quien pidió reducir, no eliminar: los fundidos de opacidad son seguros y este reset se los lleva por delante.

La postura razonable: úsalo como red de seguridad en una base de código grande donde no puedes auditar cada animación, y trata cada componente que revises quitándolo de su alcance y dándole una alternativa reducida de verdad.

🛑
El error de dejar contenido inalcanzable

Si una animación es la que revela contenido —un acordeón que anima su altura, un elemento que entra desde fuera de la pantalla— desactivarla sin más puede dejar el contenido invisible o fuera del viewport para siempre. Comprueba siempre que el estado final es correcto sin la animación, no solo que la animación no se ejecuta. Es la forma más habitual de que un intento de mejorar la accesibilidad la rompa.

Desde JavaScript

La misma consulta está disponible con matchMedia, y conviene escuchar los cambios porque el usuario puede activar el ajuste con la página abierta:

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

function aplicar() {
  document.documentElement.dataset.movimiento = mq.matches ? 'reducido' : 'completo';
}

aplicar();
mq.addEventListener('change', aplicar);

Con ese atributo en la raíz puedes ramificar tanto en CSS como en el código que dispara animaciones imperativas. Cualquier animación que lances desde JavaScript —incluida la API de animaciones web y las transiciones de vista— debe consultar esto antes de ejecutarse; la media query solo gobierna lo que está escrito en CSS.

Una preferencia del usuario es un dato, no una opinión sobre tu diseño

El instinto defensivo ante estas consultas es tratarlas como una degradación: “si el usuario pide menos movimiento, se pierde parte del diseño”. Ese encuadre es el que produce implementaciones perezosas, porque nadie invierte esfuerzo en diseñar cuidadosamente una versión que considera peor. El encuadre correcto es que la preferencia es información sobre el usuario que tú no tenías, exactamente igual que el ancho de su pantalla o el idioma de su navegador, y que la versión reducida es un diseño de pleno derecho que hay que diseñar. Nadie entrega una versión móvil sin mirarla porque “es la degradada”; la versión con movimiento reducido merece el mismo trato. Y hay un argumento pragmático que suele convencer a quien no se convence con el argumento ético: la versión reducida es la que verán también todos los que tengan el ajuste puesto por otro motivo —quien lo activó para ahorrar batería, quien usa un dispositivo lento, quien trabaja en una máquina corporativa con animaciones desactivadas por política—. No es un caso marginal, es un segmento, y es un segmento al que nadie más está prestando atención.