Repository navigation
[iOS] TextInput caret and typed text drawn above centre when its view is reused from a larger-font TextInput that was given a value #59184
Description
Description
On iOS, a single-line TextInput draws its caret and typed text above its vertical centre while it is focused, when its native view was last used by a TextInput with a larger font that was given a value. It stays that way for as long as the view lives, which in practice means until the app restarts. Fabric recycles RCTTextInputComponentViews, so in an app this shows up "sometimes": after a screen with a large-font field (a price, an amount) closes, the next single-line fields can get its view. The same field is centred again once it loses focus; only editing is off.
Cause. When a value is written into a field, -[RCTTextInputComponentView _setAttributedString:] restores the caret through -[RCTUITextField setSelectedTextRange:notifyDelegate:], also when the field is not first responder. UIKit answers every selection change with -[UIFieldEditor scrollSelectionToVisible:], so the field editor (the scroll view a UITextField edits through) scrolls to the caret even while it is detached from the field:
- On
Insert,RCTPerformMountInstructionscallsupdateState:beforeupdateLayoutMetrics:. So when a recycled view gets its new owner's value, theRCTUITextFieldand its field editor still have the previous owner's frame. With a 17 pt field (height 42) as the previous owner and a 32 pt field as the new one, the editor is 42 pt tall and its content 57 pt, and the caret restore scrolls it tocontentOffset.y = 4.67. The 32 pt field is never focused. - Nothing resets that offset. The field editor survives recycling together with its
RCTUITextField,prepareForRecycleclears the text but not the editor's offset, and when a later owner begins editing, UIKit keeps the offset although the content (336 × 42) now fits the editor (336 × 42). - The next single-line field that gets the view edits through that editor, so its caret and typed text are drawn 4.67 pt above centre while it is focused.
A fresh view is not affected: if the 32 pt field is the first owner of its view (button 1 in the reproducer skipped), no offset is left behind.
Suggested fix. In _setAttributedString:, restore the selection only when _backedTextInputView.isFirstResponder. A field nobody is editing shows no caret, so there is nothing to keep visible. The scrollRangeToVisible: call right after it is already a no-op for single-line fields (#38679); the selection restore reaches the same kind of scroll through UIKit. If the restore has to stay for fields that are not being edited, then the field editor's vertical offset needs a reset, either in prepareForRecycle or when editing begins.
Workaround. None in JavaScript: the offset lives in UIKit's field editor. Our app ships a small native module that puts a single-line field editor's vertical offset back to zero when editing begins and its content fits:
NotificationCenter.default.addObserver(
forName: UITextField.textDidBeginEditingNotification, object: nil, queue: .main
) { notification in
guard let field = notification.object as? UITextField else { return }
field.layoutIfNeeded()
for case let editor as UIScrollView in field.subviews
where editor.contentOffset.y != 0 && editor.contentSize.height <= editor.bounds.height {
editor.contentOffset.y = 0
}
}Related. #59132 is another state that outlives the recycling of RCTTextInputComponentView (the placeholder's paragraph style). #38679 made -[RCTUITextField scrollRangeToVisible:] a no-op, so that single-line fields don't scroll after a value change.
Steps to reproduce
- Clone the reproducer,
yarn install,cd ios && bundle exec pod install,yarn ios. - Tap 1. Mount a 17 pt field: mounts
<TextInput placeholder="Type here" style={{ height: 42, paddingHorizontal: 16, fontSize: 17 }} />. - Tap 2. Replace it with a 32 pt value: mounts
<TextInput value="2 000 Kč" style={{ height: 40, fontSize: 32, fontWeight: '800', textAlign: 'center' }} />, which is never focused. - Tap 3. Replace that with a 17 pt field: mounts the field from step 2 again, with
autoFocus, and it reuses the same native view. Type something. - The caret and the typed text of the reused field sit above the red centre line. The reference field at the top is identical but was mounted fresh: focus it and type, and its text is centred. The reused field's text is centred too once it loses focus.
React Native Version
0.88.0
Affected Platforms
Runtime - iOS, Build - MacOS
Areas
Fabric - The New Renderer
Output of npx @react-native-community/cli info
System:
OS: macOS 27.0.1
CPU: (15) arm64 Apple M5 Pro
Memory: 2.79 GB / 48.00 GB
Shell:
version: "5.9"
path: /bin/zsh
Binaries:
Node:
version: 24.15.0
path: ~/.local/state/fnm_multishells/88177_1792390766986/bin/node
Yarn:
version: 1.22.22
path: ~/.local/state/fnm_multishells/88177_1792390766986/bin/yarn
npm:
version: 11.12.1
path: ~/.local/state/fnm_multishells/88177_1792390766986/bin/npm
Watchman:
version: 2026.09.21.00
path: /opt/homebrew/bin/watchman
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.2614.16204760
Xcode:
version: 27.0/27A266a
path: /usr/bin/xcodebuild
Languages:
Java:
version: 21.0.5
path: /usr/bin/javac
Ruby:
version: 4.0.7
path: /opt/homebrew/bin/ruby
npmPackages:
"@react-native-community/cli":
installed: 20.2.0
wanted: 20.2.0
react:
installed: 19.3.0
wanted: 19.3.0
react-native:
installed: 0.88.0
wanted: 0.88.0
react-native-macos: Not Found
npmGlobalPackages:
"*react-native*": Not Found
Android:
hermesEnabled: true
newArchEnabled: true
iOS:
hermesEnabled: true
newArchEnabled: true
Stacktrace or Logs
No crash or log output; the bug is visual. Native state read with LLDB in the reproducer.
Step 3 (the 32 pt value is written), breakpoint on -[UIFieldEditor setContentOffset:] with a non-zero y
new offset: {0, 4.67}
UIFieldEditor: 0x104e38a00, frame (16 0; 336 42), contentSize {336, 57}, superview nil
backtrace:
-[UIFieldEditor setContentOffset:]
-[UIScrollView _setContentOffset:animated:animationCurve:animationAdjustsForContentOffsetDelta:animation:animationConfigurator:]
-[UIFieldEditor setContentOffset:animated:]
-[UIScrollView scrollRectToVisible:animated:]
-[UIFieldEditor _scrollRangeToVisible:animated:]
-[UIFieldEditor scrollSelectionToVisible:]
-[UIFieldEditor textInputDidChangeSelection:]
-[UITextInputController _sendDelegateChangeNotificationsForText:selection:replacement:characterRange:]
...
-[UITextInputController setSelectedTextRange:]
-[UIFieldEditor setSelectedTextRange:]
-[UITextField setSelectedTextRange:]
-[RCTUITextField setSelectedTextRange:notifyDelegate:]
-[RCTTextInputComponentView _setAttributedString:]
-[RCTTextInputComponentView updateState:oldState:]
RCTPerformMountInstructions(...)
-[RCTMountingManager performTransaction:]
Step 4, reused field focused (same field editor instance)
RCTUITextField: frame (0 0; 368 42)
UIFieldEditor: 0x104e38a00, frame (16 0; 336 42), contentSize {336, 42}, contentOffset {0, 4.67}
Reference field focused (fresh view)
RCTUITextField: frame (0 0; 368 42)
UIFieldEditor: frame (16 0; 336 42), contentSize {336, 42}, contentOffset {0, 0}
Measured on the screenshots, relative to the red centre line:
reference field, focused: caret -0.17 pt, text 0.00 pt
reused field, focused: caret -4.83 pt, text -4.67 pt
reused field, not focused: text 0.00 pt
MANDATORY Reproducer
https://github.com/jacoporicare/rn-caret-offset-reused-view
Screenshots and Videos
After step 4 on the iOS 27 simulator; we see the same on a physical iPhone in our app. The red line marks each field's vertical centre. Left: the reused field is focused, and its caret and text sit above the line. Right: the reference field is focused instead; its text is centred, and the reused field's text is centred again while it is not focused.

Activity
Fix off-center caret and text in a recycled TextInput on iOS (react#5…