Describe the bug
KeyboardAvoidingView with automaticOffset silently falls back to the parent-relative y whenever the native measurement rejects, and it only measures once per layout. On a screen that never lays out again, the wrong value sticks for the lifetime of the screen and the view avoids the keyboard by absoluteY - relativeY too little.
The chain:
automaticOffset resolves the window position inside onLayout, via KeyboardControllerNative.viewPositionInWindow(tag) (src/components/KeyboardAvoidingView/index.tsx).
- On the New Architecture the native side looks the view up with
[UIApplication.sharedApplication.activeWindow viewWithTag:tag] and rejects with E_VIEW_NOT_FOUND / "Could not find view for tag" when !view || !view.superview (ios/KeyboardControllerModule.mm). That is exactly the case right after mount, while the view is not yet in the active window's hierarchy — e.g. during a navigation transition.
- The JS
.catch then calls runOnUI(onLayoutWorklet)(layout) with the raw layout, whose y is relative to the parent. The padding is computed as if the view sat at the top of its parent.
- Screens that receive a later layout pass re-run the measurement and self-correct, so this is invisible most of the time. Screens that do not — typically one that opens with an
autoFocus input, so the keyboard is already animating in at mount — keep the wrong offset forever.
Nothing is logged, so the failure mode is a plausible-looking layout that is short by a fixed amount.
Code snippet
// Inside a container that is offset from the top of the window (a card with a top
// margin is enough). The keyboard opens during the entrance transition because of
// autoFocus, and the layout never changes afterwards.
<KeyboardAvoidingView behavior="padding" automaticOffset style={{ flex: 1 }}>
<ScrollView keyboardShouldPersistTaps="handled">
<TextInput autoFocus />
<TextInput />
<Button title="Save" />
<Button title="Cancel" />
</ScrollView>
</KeyboardAvoidingView>
Repo for reproducing
I do not have a standalone reproduction repo to attach, sorry — this comes from a production app, where two screens sharing the same container geometry differ only in what opens the keyboard. I am happy to test a patch against those screens if that helps.
To Reproduce
Steps to reproduce the behavior:
- Render a
KeyboardAvoidingView (behavior="padding", automaticOffset) inside a container that is offset from the top of the window.
- Navigate to that screen, with a
TextInput that has autoFocus, so the keyboard starts opening while the view is not yet in the active window.
- Leave the layout unchanged afterwards.
- See the bottom of the content stay behind the keyboard: the applied padding is short by exactly
absoluteY - relativeY, permanently.
Expected behavior
automaticOffset should end up with the window position whenever it is available, rather than degrading silently to a different coordinate space on the first attempt.
Possible directions, in increasing order of effort:
- Retry the native call on the next frame(s) before falling back.
- Re-measure when the keyboard is about to appear (
KeyboardEvents.willShow) and not only on layout — that is the moment the value is actually needed, and by then the view is reliably in the hierarchy.
- On Fabric, resolve the view through the component-view registry / surface presenter instead of
viewWithTag: on the active window, which cannot see views that are not currently in that window.
- At the very least, warn in
__DEV__ when the fallback kicks in, so the failure is not silent.
Screenshots
None on purpose: any on-screen instrumentation is itself part of the layout it is measuring, so a screenshot of it proves less than the numbers. These come from the KeyboardAvoidingView's own values, read on the native side.
Two screens, same container geometry, identical inputs (window height 844, keyboard 335, layout y 64 relative / 175 in window, height 669):
| screen |
keyboard opened by |
resulting scroll viewport |
applied padding |
uses |
| A |
user tapping the input after mount |
310 |
330 |
window y (correct) |
| B |
autoFocus at mount |
421 |
219 |
relative y (wrong, 111pt short) |
219 is exactly 64 + 669 - (844 - 335), i.e. the formula fed with the relative y. The viewport is a function of the padding alone, so it does not depend on how tall the content is.
Instrumenting viewPositionInWindow in the same component shows why:
- called once at mount: rejects with
Could not find view for tag on both screens;
- called again ~1s later: resolves with
y: 175 on both.
So the mount-time call rejects everywhere. The only difference is that screen A gets another layout pass, which repeats the measurement and corrects it, and screen B never does — so on B the last CTA stays behind the keyboard for as long as the screen is open.
Smartphone (please complete the following information):
- Desktop OS: macOS 26.5
- Device: iPhone 17e (simulator)
- OS: iOS 26.5
- RN version: 0.85.3
- RN architecture: new / fabric
- JS engine: Hermes
- Library version: 1.22.3
Additional context
Also on Expo SDK 56 with react-native-reanimated 4.5.3, release build.
Workaround, for anyone hitting this: measuring from JS is reliable and self-correcting; pass the delta as keyboardVerticalOffset and leave automaticOffset off.
const ref = useRef<View>(null);
const relativeY = useRef(0);
const [offset, setOffset] = useState(0);
const measure = useCallback(() => {
ref.current?.measureInWindow((_x, y) => {
const next = Math.max(0, Math.round(y - relativeY.current));
setOffset(prev => (prev === next ? prev : next));
});
}, []);
useEffect(() => {
// "will" fires on iOS, "did" is the Android counterpart
const subs = [
Keyboard.addListener('keyboardWillShow', measure),
Keyboard.addListener('keyboardDidShow', measure),
];
return () => subs.forEach(s => s.remove());
}, [measure]);
<KeyboardAvoidingView
ref={ref}
onLayout={e => {
relativeY.current = e.nativeEvent.layout.y;
measure();
}}
keyboardVerticalOffset={offset}
behavior="padding"
/>
Two open issues look like the same root cause:
(Reopened from #1592, which I had created through the CLI and which therefore skipped the issue template.)
Describe the bug
KeyboardAvoidingViewwithautomaticOffsetsilently falls back to the parent-relativeywhenever the native measurement rejects, and it only measures once per layout. On a screen that never lays out again, the wrong value sticks for the lifetime of the screen and the view avoids the keyboard byabsoluteY - relativeYtoo little.The chain:
automaticOffsetresolves the window position insideonLayout, viaKeyboardControllerNative.viewPositionInWindow(tag)(src/components/KeyboardAvoidingView/index.tsx).[UIApplication.sharedApplication.activeWindow viewWithTag:tag]and rejects withE_VIEW_NOT_FOUND/ "Could not find view for tag" when!view || !view.superview(ios/KeyboardControllerModule.mm). That is exactly the case right after mount, while the view is not yet in the active window's hierarchy — e.g. during a navigation transition..catchthen callsrunOnUI(onLayoutWorklet)(layout)with the raw layout, whoseyis relative to the parent. The padding is computed as if the view sat at the top of its parent.autoFocusinput, so the keyboard is already animating in at mount — keep the wrong offset forever.Nothing is logged, so the failure mode is a plausible-looking layout that is short by a fixed amount.
Code snippet
Repo for reproducing
I do not have a standalone reproduction repo to attach, sorry — this comes from a production app, where two screens sharing the same container geometry differ only in what opens the keyboard. I am happy to test a patch against those screens if that helps.
To Reproduce
Steps to reproduce the behavior:
KeyboardAvoidingView(behavior="padding",automaticOffset) inside a container that is offset from the top of the window.TextInputthat hasautoFocus, so the keyboard starts opening while the view is not yet in the active window.absoluteY - relativeY, permanently.Expected behavior
automaticOffsetshould end up with the window position whenever it is available, rather than degrading silently to a different coordinate space on the first attempt.Possible directions, in increasing order of effort:
KeyboardEvents.willShow) and not only on layout — that is the moment the value is actually needed, and by then the view is reliably in the hierarchy.viewWithTag:on the active window, which cannot see views that are not currently in that window.__DEV__when the fallback kicks in, so the failure is not silent.Screenshots
None on purpose: any on-screen instrumentation is itself part of the layout it is measuring, so a screenshot of it proves less than the numbers. These come from the
KeyboardAvoidingView's own values, read on the native side.Two screens, same container geometry, identical inputs (window height 844, keyboard 335, layout
y64 relative / 175 in window, height 669):y(correct)autoFocusat mounty(wrong, 111pt short)219 is exactly
64 + 669 - (844 - 335), i.e. the formula fed with the relativey. The viewport is a function of the padding alone, so it does not depend on how tall the content is.Instrumenting
viewPositionInWindowin the same component shows why:Could not find view for tagon both screens;y: 175on both.So the mount-time call rejects everywhere. The only difference is that screen A gets another layout pass, which repeats the measurement and corrects it, and screen B never does — so on B the last CTA stays behind the keyboard for as long as the screen is open.
Smartphone (please complete the following information):
Additional context
Also on Expo SDK 56 with
react-native-reanimated4.5.3, release build.Workaround, for anyone hitting this: measuring from JS is reliable and self-correcting; pass the delta as
keyboardVerticalOffsetand leaveautomaticOffsetoff.Two open issues look like the same root cause:
automaticOffsetsometimes wrong on a warm start from a push notification: the screen mounts while the view is not in the active window yet, so the first (and only) measurement rejects.automaticOffsetunder-lifts on a screen pushed inside an iOS pageSheet modal, short by a fixed inset: same signature as a relative-vs-window mismatch.(Reopened from #1592, which I had created through the CLI and which therefore skipped the issue template.)