Animación accesible: movimiento reducido y WCAG
Por el equipo de Charmotion · Actualizado el

Una animación accesible respeta el ajuste Reducir movimiento del usuario y se puede pausar. Comprueba prefers-reduced-motion: reduce en la web o accessibilityReduceMotion en SwiftUI y reemplaza el movimiento en loop por una pose estática. Según el criterio de éxito 2.2.2 de WCAG 2.2, el movimiento automático que dura más de cinco segundos junto a otro contenido necesita un control para pausarlo, detenerlo u ocultarlo.
Puntos clave
prefers-reduced-motiontiene dos valores,no-preferenceyreduce, y es compatible con todos los navegadores modernos.- Cuando el valor es
reduce, reemplaza los loops del personaje por una pose estática o un fundido corto. - WCAG 2.2.2 (nivel A): el movimiento con reproducción automática que dura más de cinco segundos necesita una forma de pausarlo, detenerlo u ocultarlo.
- Las animaciones de carga pueden considerarse esenciales cuando no es posible ninguna interacción durante la espera.
- Nunca hagas destellos más de tres veces por segundo (criterio 2.3.1, nivel A).
¿Qué es prefers-reduced-motion?
prefers-reduced-motion es una media feature de CSS que le indica a una página si el usuario pidió a su sistema operativo minimizar el movimiento no esencial. Tiene dos valores: no-preference y reduce. Según MDN, está disponible en todos los navegadores desde enero de 2020.
Los usuarios no lo configuran en el navegador. Lo configuran en el sistema operativo, y el navegador lo transmite:
- iOS y iPadOS: Configuración > Accesibilidad > Movimiento > Reducir movimiento.
- macOS: Ajustes del Sistema > Accesibilidad > Pantalla > Reducir movimiento.
- Windows 11: Configuración > Accesibilidad > Efectos visuales > Efectos de animación (desactivado).
- Android 9+: Ajustes > Accesibilidad > Quitar animaciones.
Las personas lo activan por distintos motivos. En quienes tienen trastornos vestibulares, los movimientos amplios, el parallax y los zooms pueden causar mareos o náuseas. A otras personas el movimiento constante las distrae al leer. "Reducir" no tiene que significar "nada". Un fundido suave de opacidad o un cambio de color suele estar bien. Lo que conviene quitar son los desplazamientos amplios, los giros, los rebotes y el movimiento en loop que nunca se detiene.
¿Cómo admito el movimiento reducido en CSS y JavaScript?
En CSS, escribe la animación completa por defecto y desactívala dentro de la media query. Una alternativa más segura es envolver la animación en @media (prefers-reduced-motion: no-preference) para que el movimiento solo se agregue cuando el usuario no tenga ninguna preferencia. El primer patrón se ve así:
.mascot { animation: bob 3s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.mascot { animation: none; }
}
En JavaScript, usa matchMedia. Escucha también los cambios, porque el usuario puede cambiar el ajuste mientras tu página está abierta:
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
function applyMotionPreference() {
document.documentElement.classList.toggle("reduce-motion", query.matches);
}
applyMotionPreference();
query.addEventListener("change", applyMotionPreference);
En React, envuelve eso en un hook pequeño que devuelva query.matches y vuelva a renderizar cuando cambie, y luego usa ese valor como condición en cada lugar donde inicies una animación.
En los loops de video, comprueba la preferencia antes de reproducir. Si es reduce, deja el <video> pausado en su fotograma de póster, o renderiza la imagen de póster en lugar del video.
¿Cómo reemplazo un loop de Rive o de video por una pose estática?
El patrón más sencillo, que funciona con cualquier formato: ten una imagen fija de la pose neutral del personaje y muéstrala en lugar de la animación cuando el movimiento reducido esté activado.
En un video, esa imagen fija es el fotograma de póster. Exporta el primer fotograma del loop en reposo (idle) como PNG o WebP transparente para que la versión con movimiento reducido se vea idéntica, solo que sin moverse.
Con un personaje de Rive tienes dos opciones:
- Muestra una imagen estática de la pose en reposo en lugar de montar el canvas de Rive. Así también evitas cargar el runtime, lo que ahorra datos a esos usuarios.
- Mantén Rive, pero fija un estado tranquilo. Pon la máquina de estados en su loop más calmado y omite las reacciones puntuales que rebotan o se deslizan. Las máquinas de estados de Rive pueden "asentarse" (settle), es decir, el runtime deja de avanzar cuando nada cambia.
Si usas un archivo de Charmotion, los loops idle (mood 0) y sleeping (mood 8) son los estados más tranquilos para fijar. Elijas el camino que elijas, sigue transmitiendo la información que comunicaba el movimiento. Si una animación de éxito le indica al usuario que un pago se completó, conserva el texto o el ícono que lo dice.
¿Cómo admito Reducir movimiento en iOS?
SwiftUI expone la configuración del usuario mediante el valor de entorno accessibilityReduceMotion. Léelo en cualquier vista y elige la rama estática cuando valga true:
struct MascotView: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
if reduceMotion {
Image("mascot-idle-still")
.resizable()
.scaledToFit()
} else {
AnimatedMascot() // your Rive view or video player
}
}
}
El valor se actualiza en vivo, así que la vista cambia si el usuario modifica la Configuración mientras tu app está en ejecución. Aplica la misma regla a las transiciones que agregues tú: reemplaza un deslizamiento o un efecto spring por un fundido cruzado, o por ninguna animación.
En la web, Safari en iOS informa la misma configuración mediante prefers-reduced-motion, así que un solo ajuste del sistema operativo cubre tu app y tu sitio.
¿Mi mascota de marca en loop necesita un botón de pausa?
A menudo, sí. El criterio de éxito 2.2.2 de WCAG 2.2, Pause, Stop, Hide (Pausar, detener, ocultar), es de nivel A, el mínimo que exigen la mayoría de las políticas de accesibilidad. Se aplica al contenido en movimiento, parpadeante o con desplazamiento que cumple estas tres condiciones:
- Empieza automáticamente.
- Dura más de cinco segundos.
- Se presenta en paralelo con otro contenido.
Si se cumplen las tres, los usuarios necesitan un mecanismo para pausarlo, detenerlo u ocultarlo, salvo que el movimiento sea esencial. Una mascota en reposo (idle) que se repite sin fin junto al texto de tu página cumple las tres.
Formas de cumplir:
- Que se detenga sola. Reproduce el loop durante unos pocos ciclos, menos de cinco segundos en total, y luego déjala en un fotograma fijo.
- Agrega un control visible. Un botón de pausa cerca de la animación que llame a
video.pause()o que reemplace el canvas de Rive por la imagen fija. - Permite ocultarla. Un control para descartarla elimina el movimiento por completo.
Respetar prefers-reduced-motion es una buena práctica, pero por sí solo no cumple el criterio 2.2.2. Muchas personas a las que el movimiento distrae nunca han encontrado ese ajuste del sistema operativo.
¿Las animaciones de carga están exentas?
Pueden estarlo. El documento Understanding del W3C para el SC 2.2.2 dice que una animación que se ejecuta durante una fase de precarga "puede considerarse esencial si la interacción no puede ocurrir durante esa fase para ningún usuario". El razonamiento: ocultar la única señal de progreso podría hacer creer a los usuarios que la app se congeló.
Esa excepción es limitada. Cubre una pantalla de carga completa o una espera que bloquea la interacción. No cubre una mascota que mantiene un loop de carga o de "pensando" en una esquina mientras el usuario todavía puede leer y escribir. En ese caso, mantén el loop sutil y detenlo cuando termine la tarea, o bien ofrece una pausa.
Vincula los loops de carga a trabajo real. Inicia el loop cuando empiece la solicitud y termínalo con una reacción puntual de éxito o error cuando finalice. Un loop que dura más que la tarea es a la vez un problema de accesibilidad y una señal de estado engañosa. Para recomendaciones de tiempos en las esperas, consulta Duración de animaciones y UX de carga.
¿Y las animaciones de interacción y los destellos?
Hay dos criterios más que se aplican a la animación de personajes.
SC 2.3.3 Animation from Interactions (nivel AAA)
"La animación de movimiento activada por la interacción puede desactivarse, salvo que la animación sea esencial." Cubre cosas como una mascota que salta cuando se toca un botón o una tarjeta que cruza la pantalla volando. El nivel AAA rara vez es obligatorio, pero respetar prefers-reduced-motion en estas reacciones lo cumple con poco esfuerzo. Conserva la respuesta visual; solo conviértela en un fundido, un cambio de color o una imagen fija en lugar de movimiento.
SC 2.3.1 Three Flashes or Below Threshold (nivel A)
El contenido no debe destellar más de tres veces en cualquier período de un segundo, salvo que se mantenga por debajo de los umbrales de destello general y de destello rojo. Los destellos rápidos pueden provocar convulsiones en personas con epilepsia fotosensible. Presta atención a esto en las animaciones de error y de atención: un parpadeo rojo o un brillo estroboscópico es justo donde se cuela. Mantén los parpadeos lentos y suaves.
Las mascotas en la interfaz de un producto rara vez necesitan destellar. Comunica la urgencia con un solo pulso o con un mensaje claro.
Cómo hacer accesible una animación de personaje en loop
- Prepara una pose fija. Exporta un PNG o WebP con fondo transparente del fotograma neutro o en reposo (idle) del personaje.
- Detecta la preferencia. Usa
matchMedia("(prefers-reduced-motion: reduce)")en la web oaccessibilityReduceMotionen SwiftUI, y escucha los cambios. - Reemplaza el movimiento por la imagen fija. Cuando el movimiento reducido esté activado, muestra la imagen fija en lugar del canvas de Rive o del video, o mantén un estado tranquilo sin reacciones puntuales.
- Agrega un control de pausa. Para los loops que empiezan automáticamente y duran más de cinco segundos junto a otro contenido, agrega un botón visible para pausar, detener u ocultar (WCAG 2.2.2).
- Limita los loops de carga. Inicia los loops de carga cuando empiece el trabajo y termínalos con una reacción de éxito o error cuando finalice.
- Revisa los destellos. Asegúrate de que nada destelle más de tres veces por segundo, sobre todo en los estados de error y de atención.
Preguntas frecuentes
¿prefers-reduced-motion significa que debo quitar toda la animación?
No. Reduce los movimientos grandes, el parallax, los giros, los rebotes y los loops infinitos. Los fundidos de opacidad suaves, los cambios de color y una pose estática del personaje suelen estar bien. Conserva la información que transmitía el movimiento, como un estado de éxito o error, mediante texto, íconos o una imagen fija.
¿Respetar prefers-reduced-motion es suficiente para WCAG?
Por sí solo, no. Es la forma habitual de cumplir el SC 2.3.3 (AAA), pero el SC 2.2.2 (nivel A) sigue exigiendo un mecanismo para pausar, detener u ocultar el movimiento con reproducción automática de más de cinco segundos que se muestra junto a otro contenido, porque muchos usuarios nunca cambian el ajuste del sistema operativo.
¿Cómo pruebo el movimiento reducido?
Activa Reducir movimiento en los ajustes de accesibilidad de iOS o macOS, desactiva Efectos de animación en Windows 11 o activa Quitar animaciones en Android. En Chrome DevTools también puedes emular la media feature desde el panel Rendering. Luego confirma que los loops se detienen y que aparece la pose fija.
Respuestas rápidas de esta guía
Fuentes
- prefers-reduced-motion, MDN Web Docs
- accessibilityReduceMotion, Apple Developer Documentation
- Understanding SC 2.2.2: Pause, Stop, Hide, W3C WAI
- Understanding SC 2.3.3: Animation from Interactions, W3C WAI
- Understanding SC 2.3.1: Three Flashes or Below Threshold, W3C WAI
- State machines (web runtime), Rive