Skip to content

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

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

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

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Problem

The native runtime evaluates prefers-color-scheme but not prefers-reduced-motion. Tailwind compiles motion-reduce:* to @media (prefers-reduced-motion: reduce) and motion-safe:* to @media (prefers-reduced-motion: no-preference), so both variants compile and then never apply on a device, whatever its accessibility setting.

A keyword value is also compared as written, so (prefers-reduced-motion: REDUCE) and (prefers-color-scheme: DARK) never match. Keywords are ASCII case-insensitive (css-values-4 §4.1), and lightningcss hands an unknown feature's ident through unfolded.

Solution

  • reactivity.ts adds a reduceMotion observable. It is seeded from AccessibilityInfo.isReduceMotionEnabled() and kept live from reduceMotionChanged, as colorScheme is from Appearance. A platform without AccessibilityInfo leaves it false instead of throwing, because every media feature imports this module.
  • prefers-reduced-motion: <value> is an equality against the current preference, so an unrecognised value matches neither state.
  • (prefers-reduced-motion) in a boolean context is true under reduce and false under no-preference (MQ5 §12.1). fix: answer a condition the runtime cannot decide with unknown, not false #426 answers the other features in a boolean context.
  • The compiler lowercases an ident value.

Tests

  • src/__tests__/native/media-query.test.tsx
    • motion-reduce and motion-safe toggling both ways, composed with and, negated, as a bare boolean and with an unknown value.
    • A table of 13 queries under each motion preference in each colour scheme, as Chromium 153, Firefox 155 and WebKit 26.6 evaluate them with the preference emulated.
  • src/__tests__/native/reduce-motion-init.test.ts: a missing getter, a rejecting getter and a working one with its change event.
  • src/__tests__/compiler/compiler.test.tsx: the conditions motion-reduce: and motion-safe: compile to.

On main, 62 of the 90 cases in the three files fail; all pass here. Five mutations each fail their own cases: the bare feature never matching, the two values swapped, a case-sensitive ident, a seed that never applies, and another feature matching in a boolean context.

Verification

On Windows with Node 26:

  • yarn lint clean
  • yarn typecheck clean
  • yarn test --maxWorkers=2 --coverage: 1394 passed, 4 failed. The 4 are the babel cases that also fail on main on this machine.
  • yarn build clean
  • yarn example expo export --platform web exported
  • nothing unstaged after either build

No existing issue tracks this. I searched for reduced motion, prefers-reduced-motion, motion-reduce and motion-safe.

Known limits

Merge order

It also shares lines with #425 (__tests__/native/media-query.test.tsx, native/conditions/media-query.ts), #427 (native/reactivity.ts), #459 (native/conditions/media-query.ts); whichever lands second rebases.

Base

Re-written on main (a5002c5).

@YevheniiKotyrlo

YevheniiKotyrlo commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — Both rows paint the same — the media feature evaluates false on every device, so neither variant fires.

FIXED — Exactly one row is green. Which one tracks the OS reduce-motion setting shown above it.

before — stock 3.0.7 after — with this PR

Both frames come from the same device in the same run (Android 36 emulator, 1140×2400 @ 480dpi). before is stock 3.0.7 rather than "this build minus this PR", so one unrelated difference is visible and worth naming rather than leaving you to spot it: the compiler's inlineRem defaults to 14 and our build sets it to 16, so every rem-derived length in the before frame renders at 87.5% of the after one — smaller type, tighter spacing, a shorter probe box. That is a different fix, not this one. Read the pair as the subject moving against the control the scene renders beside it, which this PR does not change.

Each frame carries a build-probe width=<dp> line — a rem-derived box that resolves differently on a patched build. The capture harness reads it off the device and refuses to save a frame whose probe disagrees with the variant it claims, so a before image cannot silently be a second after.

@YevheniiKotyrlo

YevheniiKotyrlo commented Aug 18, 2026 •

Copy link
Copy Markdown
Contributor Author

@danstepanov — I've added before/after device captures to 19 of my 26 open PRs here. Each pair is the same device in the same run, differing only in whether the patch is applied, so the difference should be quick to eyeball.

The other seven (#390, #410, #427, #428, #429, #430, #432) have nothing visual to show — they're build, typing and resolution fixes.

No rush at all, and happy to rebase or split any of them if that makes them easier to take.

A keyword is ASCII case-insensitive (css-values-4 §2.5), and lightningcss
hands an unknown feature's ident through as authored, so
`(prefers-color-scheme: DARK)` and `(prefers-reduced-motion: REDUCE)`
matched nothing. The compiler lowercases an ident value. The native
suite checks every row of a browser table: twelve queries under each
motion preference and colour scheme, as Chromium, Firefox and WebKit
evaluate them.
A feature in a boolean context is true unless its value is zero, `none`
or one the feature defines as false (MQ4 §2.4.2), so `(prefers-color-scheme)`,
`(orientation)`, `(width)`, `(height)`, `(resolution)` and `(display-mode)`
match in browsers. The runtime answered every one of them false. The
browser table in the native suite gains a row for each.
…vewind#426

nativewind#426 answers every media feature in a boolean context from its current
value. This branch keeps the one arm its own feature needs.

This branch has not been deployed

No deployments
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