◉RORK LABJP
●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first●EXPO — EAS Observe now records native crashes (Oct 7). On SDK 57, update to 57.0.21 or later●SDK 58 — SDK 58 Beta has been out since Sep 15. The stable date is still unconfirmed●RN 0.88 — React Native 0.88.x is scheduled for Oct 12, 3 days left●Q&A — People are asking why expo-widgets render blank only in production builds●RORK — GPT-6.1 Sol was added on Sep 29, available on Pro and Max plans●NEW — Building an app for a client? Decide who owns the publishing account first
Articles/Dev Tools
⬡ Dev Tools/2026-06-23Intermediate

What Your App Should Do When Someone Turns On Reduce Motion — A Motion-Respecting Layer in Expo

When a user enables iOS Reduce Motion or Android Remove Animations, how should your app respond? Combine AccessibilityInfo with Reanimated's ReduceMotion to replace heavy motion with a calmer alternative instead of simply switching it off.

Reduce MotionAccessibility4Reanimated10Expo213React Native238

✦ Premium Article

One day a review on a wallpaper app I maintain included a single line: "the slideshow makes me feel sick." Three stars. The note was short, but as an indie developer I sat with it for a while. The transition I had built as a cross-fade actually layered in a slight zoom and some parallax. For someone with Reduce Motion enabled, that movement was not pleasant — it was a burden.

When you run several apps in parallel on your own, you keep discovering this kind of "bug you can't see from your own device." Reduce Motion was a textbook case. Users with the setting on certainly exist, and our animations were reaching them too forcefully.

This article walks through how to build a layer that returns a gentler alternative based on the user's setting — not one that strips animation away wholesale — following an Expo + Reanimated implementation.

Where the device tells you about Reduce Motion

First, let's pin down how the OS exposes this preference.

On iOS it lives under Settings → Accessibility → Motion → Reduce Motion. On Android the closest equivalent is Settings → Accessibility → Remove animations. From React Native, both are read through the same AccessibilityInfo API.

import { AccessibilityInfo } from 'react-native';
 
// Read the current state once
const enabled = await AccessibilityInfo.isReduceMotionEnabled();

The easy thing to miss is that isReduceMotionEnabled() is an asynchronous API returning Promise<boolean>. You'll be tempted to read it synchronously, but the value isn't settled on the first render. Worse, users sometimes toggle the setting while your app is open — they jump to Settings, turn it on, and come back. To follow that round trip you need a subscription, not a one-time read.

A useReducedMotion hook that follows mid-session changes

You can subscribe to changes with AccessibilityInfo.addEventListener('reduceMotionChanged', ...). Let's wrap it in a small hook.

import { useEffect, useState } from 'react';
import { AccessibilityInfo } from 'react-native';
 
export function useReducedMotion(): boolean {
  const [reduced, setReduced] = useState(false);
 
  useEffect(() => {
    let mounted = true;
 
    // Read the initial value (async, so handle it with then, not await)
    AccessibilityInfo.isReduceMotionEnabled().then((value) => {
      if (mounted) setReduced(value);
    });
 
    // Follow toggles made while the app is running
    const sub = AccessibilityInfo.addEventListener(
      'reduceMotionChanged',
      (value) => setReduced(value),
    );
 
    return () => {
      mounted = false;
      sub.remove();
    };
  }, []);
 
  return reduced;
}

The mounted flag is there so that if the component unmounts before isReduceMotionEnabled() resolves, the later setReduced never fires. It's a small move, but it quietly prevents warnings on short-lived screens.

Note that since v3.5 Reanimated ships an official useReducedMotion() hook of the same name. If you already use Reanimated, reaching for that is the straightforward choice. I'm showing the hand-rolled version because understanding the mechanism lets you explain to yourself why the first value is false and why toggles are reflected.

// If you already use Reanimated, this is enough
import { useReducedMotion } from 'react-native-reanimated';
✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦A useReducedMotion hook that subscribes to AccessibilityInfo and follows changes made while the app is running
✦Using Reanimated's ReduceMotion.System to tune behavior per animation
✦A decision table for replacing only parallax and large motion, rather than killing everything
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $15 for lifetime access
View Membership →

Related Articles

⬡ Dev Tools2026-09-10
I Added a Dim Screen Button, and iPhones Stayed Dark After the App Was Closed
In expo-brightness, setBrightnessAsync applies only to the current activity on Android, but changes the device brightness itself on iOS. Here is why the cleanup burden falls on one platform only, and a hook that restores brightness through AppState.
⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-09-01
Adding image paste with expo-paste-input, and moving the disappearing file:// URIs out of cache
How to let users paste images and GIFs into a chat input with expo-paste-input. The URIs that onPaste hands you are temporary files that can vanish before the user hits send. Here is the relocation code and the order I verify it on real devices.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links