In React Native, how do you check whether a screen reader or reduce motion is on, and keep that value current while the app runs?
answer
- a promise now, an event later
- isScreenReaderEnabled and isReduceMotionEnabled
- screenReaderChanged, reduceMotionChanged
- subscription.remove() in the effect cleanup
- first render cannot know the value
basics
~10 sCall AccessibilityInfo.isScreenReaderEnabled() or isReduceMotionEnabled(), which return promises, for the current value, and subscribe with addEventListener to screenReaderChanged or reduceMotionChanged for later changes, removing the subscription on unmount.
solid answer
~40 s`AccessibilityInfo` from `react-native` gives a snapshot plus a stream. `isScreenReaderEnabled()` and `isReduceMotionEnabled()` return a `Promise<boolean>` for the value right now; `addEventListener('screenReaderChanged', handler)` and `addEventListener('reduceMotionChanged', handler)` call the handler with a boolean whenever the user flips the setting, and return a subscription whose `remove()` I call in the effect cleanup. I put both in one `useEffect` inside a small hook, so the value starts correct and follows the user turning VoiceOver or TalkBack on mid-session; a one-off check at launch goes stale. Screens then adapt a detail, such as keeping a toast visible longer or swapping a slide for an instant change, instead of rendering a separate UI. On Android the screen-reader check reports touch exploration, the mode TalkBack switches on.
code
tsx · 38 linesimport {useEffect, useState} from 'react';
import {AccessibilityInfo} from 'react-native';
export function useReduceMotion(): boolean | null {
const [reduceMotion, setReduceMotion] = useState<boolean | null>(null);
useEffect(() => {
let active = true;
let changed = false;
const subscription = AccessibilityInfo.addEventListener(
'reduceMotionChanged',
enabled => {
changed = true;
setReduceMotion(enabled);
},
);
AccessibilityInfo.isReduceMotionEnabled()
.then(enabled => {
if (active && !changed) {
setReduceMotion(enabled);
}
})
.catch(() => {
if (active && !changed) {
setReduceMotion(false);
}
});
return () => {
active = false;
subscription.remove();
};
}, []);
return reduceMotion;
}go deeper
Recall that the queries return promises, the change events deliver booleans, and the subscription returned by addEventListener must be removed in the effect cleanup.
Explain why a launch-time check goes stale, how to order subscribe and query so no change is lost, and why the first render cannot know the value.
Show how you centralise the flag in one provider, handle a rejected query, and use it to tune timing and motion instead of shipping a separate accessible screen.
Discuss where adapting to assistive settings belongs in a design system, so every team gets the same reduced-motion and screen-reader behaviour from shared components.
## What `AccessibilityInfo` gives you `AccessibilityInfo` is a module exported from `react-native` that reports the state of the device's accessibility settings to JavaScript. It answers two different questions, and a correct screen needs both: - **What is the setting now?** Query methods such as `isScreenReaderEnabled()` and `isReduceMotionEnabled()` return a **`Promise<boolean>`**. The value comes from the native side, so it is never available synchronously during the first render. - **When does it change?** `addEventListener(eventName, handler)` registers a handler that receives a **boolean** every time the user flips the setting, and returns a **subscription** object whose `remove()` method detaches it. A **screen reader** is the assistive technology that speaks the interface aloud and changes how touch works: **VoiceOver** on iOS and **TalkBack** on Android. **Reduce motion** is the system preference asking apps to cut down on movement such as sliding, zooming and parallax. ## The query and event pairs | Setting | Query (returns `Promise<boolean>`) | Change event | Platforms | |---|---|---|---| | Screen reader | `isScreenReaderEnabled()` | `screenReaderChanged` | iOS and Android | | Reduce motion | `isReduceMotionEnabled()` | `reduceMotionChanged` | iOS and Android | | Bold text | `isBoldTextEnabled()` | `boldTextChanged` | iOS only | Two platform details matter. On Android the screen-reader check reports whether **touch exploration** is on, which is the mode TalkBack switches on. And on Android the reduce-motion value is also `true` when the developer option "Transition Animation Scale" is set to "Animation off". The older event name `change` still exists but is deprecated and maps to `screenReaderChanged`. ## Keeping the value current A check done once at launch goes stale the moment the user opens Settings and turns TalkBack on while the app sits in the background. The standard shape is one effect that does both jobs: 1. Subscribe with `addEventListener` first, so no change is missed while the query is in flight. 2. Query the current value and store it in state when the promise resolves. 3. Ignore the query result if an event has already delivered a newer value, or if the component has unmounted. 4. Return a cleanup function that calls `subscription.remove()`. There is no `removeEventListener` on `AccessibilityInfo` in current React Native; the subscription object is the only way to detach. Forgetting `remove()` leaves a handler that keeps calling a state setter after the component is gone and keeps its closure alive for the life of the app. Wrapping this in a hook such as `useScreenReaderEnabled()` or `useReduceMotion()` keeps every screen consistent. If many screens need the flag, subscribe once in a provider near the root and read it from context rather than attaching one listener per screen. ## Adapting without forking the UI The point of reading these values is to change **details**, not to build a second app. Typical adaptations in a banking app: - **Reduce motion on**: replace a sliding card with an instant swap, and skip decorative looping animations built with core `Animated`. - **Screen reader on**: keep a transient toast visible until it is dismissed instead of auto-hiding it after three seconds, because the user may still be listening to something else. - **Screen reader on**: stop auto-advancing a carousel, and expose any gesture-only shortcut as an ordinary button. Rendering an entirely separate "accessible version" of a screen is the anti-pattern: two trees drift apart, one of them is always less tested, and people with partial sight who run a screen reader still look at the screen. Labels, roles and states belong on the one real tree whether or not a screen reader is detected; the flag only tunes timing and motion. ## The first-render gap Because the query is asynchronous, the first render happens before the answer arrives. Choose the initial state deliberately: - For reduce motion, starting with `false` can play an entrance animation that the user asked not to see. Starting from an **unknown** state (`null`) and holding non-essential animation until the value is known avoids that flash. - For the screen reader, starting with `false` is usually harmless, because adaptations such as a longer toast only matter after the first interaction. The promise can also **reject**, for example when the native module is unavailable, so production hooks attach a `catch` and fall back to a default instead of leaving an unhandled rejection.
- How would you share the screen-reader flag across many screens without each one subscribing?Subscribe once in a provider mounted near the root: query `isScreenReaderEnabled()`, listen for `screenReaderChanged`, and put the boolean in a React context. Screens read the context, so there is one listener, one source of truth, and no screen can forget its cleanup. A module-level store read with `useSyncExternalStore` achieves the same without a provider.
- Why might a reduce-motion hook that starts at false still play an animation the user asked to avoid?The first render happens before `isReduceMotionEnabled()` resolves, so an entrance animation started on mount runs with the default value. Start from an unknown state such as `null` and hold non-essential animation until the answer arrives, or default to the reduced version and enable motion only once the promise says it is allowed.
saying these in an interview costs you the question
- Checking isScreenReaderEnabled once at app start is enough for the whole session.
- isScreenReaderEnabled returns a boolean synchronously, so it can be read during render.
- Detach the listener with AccessibilityInfo.removeEventListener and the same handler.
- When a screen reader is on, the app should render a separate simplified screen tree.
- Reduce motion can only be detected on iOS in React Native.