Skip to content

feat(native): evaluate prefers-reduced-motion so motion-reduce / motion-safe work on native#388

Open
YevheniiKotyrlo wants to merge 2 commits into
nativewind:mainfrom
YevheniiKotyrlo:feat/prefers-reduced-motion
Open

feat(native): evaluate prefers-reduced-motion so motion-reduce / motion-safe work on native#388
YevheniiKotyrlo wants to merge 2 commits into
nativewind:mainfrom
YevheniiKotyrlo:feat/prefers-reduced-motion

Conversation

@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor

Summary

react-native-css evaluates prefers-color-scheme on native (via Appearance) but has no wiring for prefers-reduced-motion. So Tailwind's motion-reduce: / motion-safe: variants — and any authored @media (prefers-reduced-motion) rule — compile correctly and then resolve to false on every device, silently. Web works because the browser evaluates the real CSS.

This adds the missing feature, mirroring the existing prefers-color-scheme wiring exactly:

  • src/native/reactivity.ts — a reduceMotion observable seeded from AccessibilityInfo.isReduceMotionEnabled() and kept live via the reduceMotionChanged event — the mirror of colorSchemeAppearance.
  • src/native/conditions/media-query.ts — a prefers-reduced-motion case in testComparison, handling both reduce (motion-reduce:) and no-preference (motion-safe:), plus the bare @media (prefers-reduced-motion) boolean form (per CSS, bare (prefers-reduced-motion)reduce).

get(reduceMotion) subscribes the effect, so a runtime OS toggle re-renders exactly as a colorScheme change re-renders dark:.

Why it matters

motion-reduce: / motion-safe: are the documented Tailwind way to honour WCAG 2.3.3 (reduced motion). Verified against Tailwind v4.3.2 — motion-reduce:* emits @media (prefers-reduced-motion: reduce) and motion-safe:* emits @media (prefers-reduced-motion: no-preference). Today both are dead on native: every reduced-motion affordance a consumer writes is a no-op off the web.

Notes / platform caveat

  • AccessibilityInfo.isReduceMotionEnabled() is async-only (no synchronous getter, unlike Appearance.getColorScheme()), so the observable seeds false and flips when it resolves — a one-frame cold-start window, with false (motion on) as the safe default.
  • On Android the OS surface for this flag is the animation duration scale (see [Android] "Remove animations" setting isn't reflected when using AccessibilityInfo react/react-native#31221); iOS drives it directly. This mirrors React Native's own AccessibilityInfo behaviour and is out of scope here.

Test plan

  • New describe("prefers-reduced-motion") block in src/__tests__/native/media-query.test.tsx (5 tests), mirroring the existing "color scheme" test:
    • reduce (motion-reduce:) — base style by default, reduced style after reduceMotion.set(true), and back (reactive both ways).
    • no-preference (motion-safe:) — the inverse.
    • composition with prefers-color-scheme via and.
    • negation — not (prefers-reduced-motion: reduce).
    • bare boolean — @media (prefers-reduced-motion).
  • New prefers-reduced-motion test in src/__tests__/compiler/compiler.test.tsx (mirrors light-dark()) asserting the emitted media condition: ["=", "prefers-reduced-motion", "reduce" | "no-preference"] and the bare ["!!", "prefers-reduced-motion"].
  • Existing media-query / color-scheme tests unaffected.
  • yarn test, yarn typecheck, eslint, prettier --check all pass. (The three babel suites fail identically on clean main in this environment — missing --experimental-vm-modules — and are unrelated to this change.)

Only prefers-color-scheme was wired into the native media-query
evaluator (via Appearance). prefers-reduced-motion had no observable
and no evaluator case, so `motion-reduce:` / `motion-safe:` (and any
@media (prefers-reduced-motion) rule) compiled cleanly and then
resolved to false on every device -- a silent no-op off the web.

Mirror the colorScheme wiring: a reduceMotion observable seeded from
AccessibilityInfo.isReduceMotionEnabled() and kept live via the
reduceMotionChanged event, plus a prefers-reduced-motion case in
testComparison handling reduce and no-preference. Also handle the
bare `@media (prefers-reduced-motion)` boolean form (per CSS, bare is
equivalent to reduce).

Adds runtime tests (reduce, no-preference, composition, negation,
bare) mirroring the existing color-scheme test, plus a compiler test
asserting the emitted media condition.
Note that AccessibilityInfo has no synchronous getter (unlike
Appearance.getColorScheme()), so the observable seeds `false` and
flips when the async read resolves -- a brief, inherent cold-start
window. Comment-only; no behaviour change.
@YevheniiKotyrlo
YevheniiKotyrlo marked this pull request as ready for review July 23, 2026 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant