Description
On iOS with the New Architecture (Fabric), a TextInput's selectionColor can be silently lost, causing the caret / selection highlight / selection handles to render with the iOS default systemBlue instead of the provided color.
On iOS, selectionColor is mapped to the backing view's tintColor (which drives the caret, selection highlight and grabber handles). Under Fabric this tint can end up nil after the backing view is recreated, so it resolves to the window's default tint (systemBlue). The failure is intermittent, and once it happens it tends to stay broken for other inputs (which reuse recycled component views) until the app is restarted.
This is the TextInput counterpart of the already-reported Fabric/iOS tintColor-not-applied bugs for RefreshControl (#56343, #53987).
Root cause
selectionColor is applied to tintColor only behind a diff guard in RCTTextInputComponentView.mm (updateProps:oldProps:):
// React/Fabric/Mounting/ComponentViews/TextInput/RCTTextInputComponentView.mm
if (newTextInputProps.selectionColor != oldTextInputProps.selectionColor) {
_backedTextInputView.tintColor = RCTUIColorFromSharedColor(newTextInputProps.selectionColor);
}
When multiline changes, the backing view is recreated in _setMultiline::
UIView<RCTBackedTextInputViewProtocol> *backedTextInputView =
multiline ? [RCTUITextView new] : [RCTUITextField new];
RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView);
_backedTextInputView = backedTextInputView;
RCTCopyBackedTextInput (RCTTextInputUtils.mm) copies ~25 properties from the old backing view to the new one but does not copy tintColor. So the freshly created backing view starts with tintColor == nil. Because selectionColor itself did not change, the diff-guarded assignment above is skipped, leaving tintColor == nil, which resolves to the window default tint (systemBlue).
In addition, prepareForRecycle does not reset or re-assert tintColor. Since Fabric recycles RCTTextInputComponentView instances across screens, once an instance ends up with tintColor == nil while _props.selectionColor already equals the incoming value, the diff guard keeps skipping reassignment and the view keeps rendering systemBlue until the recycle pool is torn down (app restart). This matches the observed "intermittent, then global until restart" behavior.
Note: RCTCopyBackedTextInput on main still does not copy tintColor.
Steps to reproduce
- New Architecture (Fabric) enabled, iOS.
- Render multiple
TextInputs that all set the same selectionColor (e.g. selectionColor="#773BEB"), including at least one single-line and one multiline input, across different screens.
- Navigate back and forth between screens that mount/unmount these inputs so Fabric recycles the backing component views (equivalently: toggle a
TextInput's multiline on a reused view).
- Observe the caret / selection color: intermittently it renders the iOS default
systemBlue instead of the provided selectionColor; once it does, subsequently mounted inputs also show systemBlue until the app is restarted.
React Native Version
0.81.5 (root cause also present on main)
Affected Platforms
Runtime - iOS, Architecture - Fabric (New Architecture)
Expected Results
The caret, selection highlight and handles use the provided selectionColor.
Actual Results
After the backing view is recreated, the caret/selection render with the iOS default systemBlue, persisting across inputs until the app is restarted.
Proposed fix
Either (or both):
- Copy
tintColor in RCTCopyBackedTextInput, so a recreated backing view keeps the tint.
- Re-assert
_backedTextInputView.tintColor from selectionColor after _setMultiline: (and/or in prepareForRecycle) rather than only on a selectionColor diff.
Related issues
Description
On iOS with the New Architecture (Fabric), a
TextInput'sselectionColorcan be silently lost, causing the caret / selection highlight / selection handles to render with the iOS defaultsystemBlueinstead of the provided color.On iOS,
selectionColoris mapped to the backing view'stintColor(which drives the caret, selection highlight and grabber handles). Under Fabric this tint can end upnilafter the backing view is recreated, so it resolves to the window's default tint (systemBlue). The failure is intermittent, and once it happens it tends to stay broken for other inputs (which reuse recycled component views) until the app is restarted.This is the
TextInputcounterpart of the already-reported Fabric/iOStintColor-not-applied bugs forRefreshControl(#56343, #53987).Root cause
selectionColoris applied totintColoronly behind a diff guard inRCTTextInputComponentView.mm(updateProps:oldProps:):When
multilinechanges, the backing view is recreated in_setMultiline::UIView<RCTBackedTextInputViewProtocol> *backedTextInputView = multiline ? [RCTUITextView new] : [RCTUITextField new]; RCTCopyBackedTextInput(_backedTextInputView, backedTextInputView); _backedTextInputView = backedTextInputView;RCTCopyBackedTextInput(RCTTextInputUtils.mm) copies ~25 properties from the old backing view to the new one but does not copytintColor. So the freshly created backing view starts withtintColor == nil. BecauseselectionColoritself did not change, the diff-guarded assignment above is skipped, leavingtintColor == nil, which resolves to the window default tint (systemBlue).In addition,
prepareForRecycledoes not reset or re-asserttintColor. Since Fabric recyclesRCTTextInputComponentViewinstances across screens, once an instance ends up withtintColor == nilwhile_props.selectionColoralready equals the incoming value, the diff guard keeps skipping reassignment and the view keeps renderingsystemBlueuntil the recycle pool is torn down (app restart). This matches the observed "intermittent, then global until restart" behavior.Note:
RCTCopyBackedTextInputonmainstill does not copytintColor.Steps to reproduce
TextInputs that all set the sameselectionColor(e.g.selectionColor="#773BEB"), including at least one single-line and one multiline input, across different screens.TextInput'smultilineon a reused view).systemBlueinstead of the providedselectionColor; once it does, subsequently mounted inputs also showsystemBlueuntil the app is restarted.React Native Version
0.81.5 (root cause also present on
main)Affected Platforms
Runtime - iOS, Architecture - Fabric (New Architecture)
Expected Results
The caret, selection highlight and handles use the provided
selectionColor.Actual Results
After the backing view is recreated, the caret/selection render with the iOS default
systemBlue, persisting across inputs until the app is restarted.Proposed fix
Either (or both):
tintColorinRCTCopyBackedTextInput, so a recreated backing view keeps the tint._backedTextInputView.tintColorfromselectionColorafter_setMultiline:(and/or inprepareForRecycle) rather than only on aselectionColordiff.Related issues
[Fabric][iOS] RefreshControl tintColor/title props not applied on initial mounttintColorprop not respected & gets stuck on navigation #53987 —RefreshControl tintColor not respected & stuck on navigation(same stack: New Arch, RN 0.81.x, Expo 54, iOS)