Saltar al contenido
Plazoleta
· Raúl López

Reduce motion no es una casilla de accesibilidad: es diseño

En Jornia, la animación de marca es la única animación custom permitida en toda la app. Por qué eso hace que respetar reduce motion sea fácil, y qué se construyó primero para que fuera así.

El sistema de diseño de Jornia dedica una frase a definir cuánto movimiento tiene la app: "curvas estándar M3, duraciones de 150 a 300 ms, nunca más de 400". Y luego una segunda frase, que es la que importa de verdad: de todo lo que se podría animar, solo una cosa tiene permiso para llevar animación propia. Todo lo demás usa las transiciones de sistema y punto.

Esa restricción no nació pensando en accesibilidad. Nació pensando en marca. Y resultó ser la misma decisión.

La única animación con nombre propio

La firma de marca de Jornia es esto: cuando marcas una parada como hecha en la línea de tiempo, el punto del conector pulsa (8→12→8 dp) y el trazo punteado hasta la siguiente parada se dibuja. Trescientos milisegundos, un CheckCircle que entra con scale-in en vez de aparecer de golpe. Está descrita en el design system como "única animación custom permitida v1", con esa coletilla explícita de exclusividad.

Es una decisión de restricción deliberada, no de recursos: Compose no cuesta más animar cinco cosas que una. Se descartó a propósito, porque una app que anima todo no tiene firma — tiene ruido. Si quieres que una sola micro-interacción se reconozca como "esto es Jornia", el precio es que las demás no compitan por atención.

El helper se construyó antes que la animación

Aquí está el detalle que no es obvio hasta que lo ves en el plan de ejecución: el primer ítem de la lista, T1.0, no es la animación de firma. Es un helper de media hora, rememberReduceMotion(), que lee Settings.Global.ANIMATOR_DURATION_SCALE == 0f del sistema.

El orden no es casualidad. Se puso primero porque lo necesitan la animación de firma (T1.1) y "las futuras animaciones", según la propia nota del plan. No se construyó la pieza vistosa y luego se le añadió el interruptor de accesibilidad por encima, que es como suele pasar cuando reduce motion se trata como una casilla de cumplimiento: se hace la función, alguien pasa una auditoría, y entonces se retrofitea un if para que la QA de accesibilidad no lo marque.

Aquí fue al revés. El interruptor es infraestructura, y la animación es lo que se construye encima de él. Eso cambia quién tiene que acordarse de comprobarlo: si rememberReduceMotion() existe antes que la primera animación, cada animación que se escriba después nace envuelta en el helper porque no hay otra forma de escribirla que sea más rápida. Si el helper llega después, cada animación existente es un sitio donde alguien tiene que volver, acordarse y parchear.

Lo que de verdad cuesta poco al tener una sola animación

Esto es lo que la restricción de "una sola animación custom" compra de rebote: respetar reduce motion en Jornia no es una auditoría por el código buscando cada animateFloatAsState suelto. Es una comprobación en un sitio.

Compárese con lo que sería en una app que sí anima diez cosas distintas —transiciones de pantalla custom, parallax en scroll, micro-rebotes en cada botón—: ahí reduce motion es un requisito transversal que hay que verificar en diez sitios, y basta con que se te escape uno para que la promesa de accesibilidad sea falsa en la práctica aunque sea verdad en la mayoría de la app. Con una sola animación con nombre y un solo helper que la envuelve, la superficie de fallo es del tamaño de una función.

Eso no significa que el resto de la app "no tenga movimiento". Las transiciones estándar de Material 3 —navegación, Emphasized— siguen ahí, y esas las gestiona el propio sistema, no el código de Jornia. reduce motion no es competencia suya: apagar la firma de marca no dejaría la app estática, la dejaría exactamente como se ve en cualquier otra app Android que respeta la preferencia del sistema. Lo único que cambia es que el punto no pulsa y el trazo no se dibuja: pasan a su estado final directamente.

Y esa comprobación se pidió expresamente, escrita como criterio de aceptación y no como nota a pie de página: "con reduce-motion, todo es instantáneo y correcto". No "no se anima" — correcto. La diferencia importa: un interruptor de accesibilidad mal implementado a veces solo apaga la animación y se olvida de dejar el punto en su radio final o el trazo completo, y el resultado es una pantalla a medio dibujar que parece un bug de red, no una preferencia de sistema respetada.

El mismo criterio, aplicado dos veces por separado

Lo que confirma que esto es una regla del sistema de diseño y no una ocurrencia puntual de una tarea es que se aplicó dos veces, en dos entregables que no tienen nada que ver entre sí. Los skeletons de carga (T1.4) llevan un shimmer sutil —un brillo que se desplaza mientras carga la pantalla—, y también él respeta rememberReduceMotion(): con la preferencia activa, no hay shimmer, solo el bloque estático con la geometría real de la tarjeta.

Nadie pidió explícitamente "aplica reduce motion también a los skeletons". Se aplicó porque el helper ya estaba ahí y la regla —cualquier cosa que se mueva sin que el usuario haya pedido moverla, se puede apagar— es la misma para la firma de marca que para un shimmer de carga. Esa es la señal de que una restricción de diseño se ha convertido en reflejo del equipo: se repite sola en un sitio donde nadie escribió el requisito.

Lo que nos llevamos

Menos animación custom es, de regalo, menos superficie de accesibilidad que vigilar. No fue el motivo por el que Jornia limitó su firma de marca a una sola pieza, pero es la consecuencia: con un solo sitio que anima algo propio, hay un solo sitio que puede desobedecer a reduce motion.

Construye el interruptor antes que lo que interrumpe. Si el helper de accesibilidad existe antes que la primera animación, cada animación nueva nace obedeciéndolo porque escribirla de otra forma sería más trabajo, no menos. Si llega después, cada animación existente es una visita pendiente.

El criterio de aceptación no es "se puede apagar", es "el estado final es correcto". Reduce motion mal hecho dibuja a medias y parece un bug. Reduce motion bien hecho salta directo al final, y es indistinguible de que nunca hubiera habido nada que animar.


Si en tu app la lista de "cosas que se animan" ya no cabe en una frase, probablemente reduce motion tampoco te cabe en un if. Escríbeme si quieres comparar cómo lo resolvisteis.

¿Tienes un problema parecido?

Cuéntanoslo. Hablas directamente con quien escribe el código.