Description
On iOS with the New Architecture, a view with backfaceVisibility: 'hidden' that is flipped with rotateY using Animated.timing(..., { useNativeDriver: true }) stays invisible after the animation, once a later render replaces its animated transform with a static one ([{ perspective: 1000 }, { rotateY: '0deg' }]).
After the first flip the view is never drawn again: the layout keeps its space and JS state reports it as the visible face, but the area is blank. The same code with useNativeDriver: false shows the view after every flip. It reproduces on every flip, on the simulator and on a physical iPhone (Release).
Steps to reproduce
- Clone https://github.com/ElJijuna/rn-backface-native-driver-repro
npm install, then cd ios && pod install && cd ..
npm start, then npm run ios on an iOS 26.x simulator. The template app doesn't adopt the UIScene lifecycle, which iOS 27 requires at launch.
- Tap the card to flip it.
- After the first flip the card area is blank.
- Set
NATIVE = false at the top of App.tsx: every flip now shows the other face.
React Native Version
0.87.1
Affected Platforms
Runtime - iOS
Output of npx @react-native-community/cli info
System:
OS: macOS 27.0.1
CPU: (18) arm64 Apple M5 Pro
Memory: 398.67 MB / 24.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 26.3.1
path: /Users/pilmee/.nvm/versions/node/v26.3.1/bin/node
Yarn: Not Found
npm:
version: 11.16.0
path: /Users/pilmee/.nvm/versions/node/v26.3.1/bin/npm
Watchman: Not Found
Managers:
CocoaPods:
version: 1.17.0
path: /opt/homebrew/bin/pod
SDKs:
iOS SDK:
Platforms:
- DriverKit 27.0
- iOS 27.0
- macOS 27.0
- tvOS 27.0
- visionOS 27.0
- watchOS 27.0
Android SDK: Not Found
IDEs:
Android Studio: 2026.1 AI-261.26222.65.2613.16025427
Xcode:
version: 27.0/27A266a
path: /usr/bin/xcodebuild
Languages:
Java:
version: 17.0.20.1
path: /opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home/bin/javac
Ruby:
version: 4.0.7
path: /opt/homebrew/bin/ruby
npmPackages:
"@react-native-community/cli":
installed: 20.1.0
wanted: 20.1.0
react:
installed: 19.2.3
wanted: 19.2.3
react-native:
installed: 0.86.3
wanted: 0.86.3
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
info React Native v0.87.1 is now available (your project is running on v0.86.3).
info Changelog: https://github.com/facebook/react-native/releases/tag/v0.87.1
info Diff: https://react-native-community.github.io/upgrade-helper/?from=0.86.3&to=0.87.1
info For more info, check out "https://reactnative.dev/docs/upgrading?os=macos".
Stacktrace or Logs
No crash, exception, red box or warning is produced. The app keeps running normally.
The failure is visual only: after the native-driven flip finishes, the incoming face is not drawn.
JS state at that point (verified in the reproducer's title text):
faces = { front: 1, back: null } -> the face renders with a static
transform: [{ perspective: 1000 }, { rotateY: '0deg' }]
Expected: face 1 visible. Actual: empty area (layout space is kept).
Same code with useNativeDriver: false -> face visible after every flip.
See the screenshots and the reproducer: https://github.com/ElJijuna/rn-backface-native-driver-repro
MANDATORY Reproducer
https://github.com/ElJijuna/rn-backface-native-driver-repro
Screenshots and Videos
Root cause (narrowed down)
I narrowed it down with the reproducer (RN 0.86.3, iOS 26.5 simulator):
- Without
backfaceVisibility: 'hidden', the resting face renders mirrored, so the native view is left at 180° while Fabric committed rotateY: '0deg'.
- Removing the final
angle.setValue(0) fixes it: the resting face is upright with useNativeDriver: true.
So the trigger is a setValue on a native-driven value in the same tick as a render that replaces the view's animated transform with a static one:
setFaces(...) re-renders the face with the static transform: [{ perspective: 1000 }, { rotateY: '0deg' }], and Fabric mounts it.
angle.setValue(0) is queued as a native operation. The view is disconnected from its props node only later (useAnimatedPropsLifecycle detaches in a queueMicrotask after the commit), so when the queue is flushed the view is still connected to the old interpolation, and the native driver writes backRotation(0) = 180deg directly to the view (synchronouslyUpdateViewOnUIThread).
- On Fabric,
RCTPropsAnimatedNode -restoreDefaultValues returns early, and the shadow tree still believes the view has 0deg, so nothing re-applies the committed value. The view stays at 180° (hidden by its backface).
It doesn't reproduce with the C++ Animated backend
Using the same reproducer, with only the release level passed to RCTReactNativeFactory(delegate:releaseLevel:) changed:
| Release level |
Result |
Stable (default) |
Bug |
Canary |
Bug |
Experimental (adds cxxNativeAnimatedEnabled + useSharedAnimatedBackend, plus enableAccessibilityOrder, enableSchedulerDelegateInvalidation, enableSwiftUIBasedFilters, preventShadowTreeCommitExhaustion) |
Works |
So it's very likely fixed by the C++ Animated backend (I didn't isolate the two Animated flags individually). The issue remains on the default iOS configuration.
Workarounds
useNativeDriver: false for this animation.
- Don't call
setValue on the native value in the same tick that replaces the animated prop with a static one.
Description
On iOS with the New Architecture, a view with
backfaceVisibility: 'hidden'that is flipped withrotateYusingAnimated.timing(..., { useNativeDriver: true })stays invisible after the animation, once a later render replaces its animatedtransformwith a static one ([{ perspective: 1000 }, { rotateY: '0deg' }]).After the first flip the view is never drawn again: the layout keeps its space and JS state reports it as the visible face, but the area is blank. The same code with
useNativeDriver: falseshows the view after every flip. It reproduces on every flip, on the simulator and on a physical iPhone (Release).Steps to reproduce
npm install, thencd ios && pod install && cd ..npm start, thennpm run ioson an iOS 26.x simulator. The template app doesn't adopt the UIScene lifecycle, which iOS 27 requires at launch.NATIVE = falseat the top ofApp.tsx: every flip now shows the other face.React Native Version
0.87.1
Affected Platforms
Runtime - iOS
Output of
npx @react-native-community/cli infoStacktrace or Logs
MANDATORY Reproducer
https://github.com/ElJijuna/rn-backface-native-driver-repro
Screenshots and Videos
Root cause (narrowed down)
I narrowed it down with the reproducer (RN 0.86.3, iOS 26.5 simulator):
backfaceVisibility: 'hidden', the resting face renders mirrored, so the native view is left at 180° while Fabric committedrotateY: '0deg'.angle.setValue(0)fixes it: the resting face is upright withuseNativeDriver: true.So the trigger is a
setValueon a native-driven value in the same tick as a render that replaces the view's animatedtransformwith a static one:setFaces(...)re-renders the face with the statictransform: [{ perspective: 1000 }, { rotateY: '0deg' }], and Fabric mounts it.angle.setValue(0)is queued as a native operation. The view is disconnected from its props node only later (useAnimatedPropsLifecycledetaches in aqueueMicrotaskafter the commit), so when the queue is flushed the view is still connected to the old interpolation, and the native driver writesbackRotation(0) = 180degdirectly to the view (synchronouslyUpdateViewOnUIThread).RCTPropsAnimatedNode -restoreDefaultValuesreturns early, and the shadow tree still believes the view has0deg, so nothing re-applies the committed value. The view stays at 180° (hidden by its backface).It doesn't reproduce with the C++ Animated backend
Using the same reproducer, with only the release level passed to
RCTReactNativeFactory(delegate:releaseLevel:)changed:Stable(default)CanaryExperimental(addscxxNativeAnimatedEnabled+useSharedAnimatedBackend, plusenableAccessibilityOrder,enableSchedulerDelegateInvalidation,enableSwiftUIBasedFilters,preventShadowTreeCommitExhaustion)So it's very likely fixed by the C++ Animated backend (I didn't isolate the two Animated flags individually). The issue remains on the default iOS configuration.
Workarounds
useNativeDriver: falsefor this animation.setValueon the native value in the same tick that replaces the animated prop with a static one.