Describe the bug
When using KeyboardAvoidingView with behavior="padding" (the same applies to any non-"height" behavior) and automaticOffset={true} on Android, the wrapped content jitters up and down repeatedly while the keyboard opens, instead of moving up smoothly once and settling. It is device-dependent (reproduces on some Android devices, not all).
Root cause appears to be in onLayoutWorklet. For any behavior other than "height", initialFrame is re-recorded on every onLayout:
if (
keyboard.isClosed.value ||
initialFrame.value === null ||
behavior !== "height" // <- always true for "padding"
) {
initialFrame.value = layout;
}
For behavior="padding", the animated paddingBottom is applied to the same view that carries onLayout. So when the view is content-sized:
- paddingBottom grows the view → its measured height (and, via automaticOffset, its y) changes.
- onLayout fires with the larger frame → initialFrame is re-recorded.
- relativeKeyboardHeight() = frame.y + frame.height − keyboardY grows → paddingBottom grows → back to step 1.
This is a positive feedback loop. keyboardY is constant (screen height, heightWhenOpened, and keyboardVerticalOffset don't change), and the keyboard progress/height shared values are stable — I verified that the only oscillating inputs are frame.value.y / frame.value.height.
automaticOffset={true} amplifies it: each onLayout resolves the frame through an async KeyboardControllerNative.viewPositionInWindow() native call, and those promises settle out of order. A late callback delivers a stale (smaller) y + height after a newer larger one already landed, which yanks the recorded frame back down — producing the visible up/down flicker.
Measured initialFrame.value across a single keyboard open (logged inside onLayoutWorklet):
y 275, height 280 → y+height 555
y 253, height 314 → y+height 567
y 246, height 335 → y+height 581
y 225, height 364 → y+height 589
y 221, height 392.5 → y+height 613
y 217, height 396.5 → y+height 613 (settles)
height is supposed to be the constant resting height but ranges 280 → 396; y + height ramps 555 → 613 with out-of-order dips in between → that ramp + jitter is the flicker.
Code snippet
import { KeyboardAvoidingView } from "react-native-keyboard-controller";
import { TextInput, View } from "react-native";
// content-sized container (no flex:1), positioned toward the bottom of the screen
function Screen() {
return (
<View style={{ flex: 1, justifyContent: "flex-end" }}>
{/* content-sized block, NOT flex:1 */}
<View style={{ padding: 16, backgroundColor: "#ddd" }}>
<TextInput placeholder="focus me" style={{ height: 44 }} />
);
}
Relevant library code (src/components/KeyboardAvoidingView/index.tsx):
const relativeKeyboardHeight = useCallback(() => {
"worklet";
const keyboardY =
screenHeight - keyboard.heightWhenOpened.value - keyboardVerticalOffset;
return Math.max(frame.value.y + frame.value.height - keyboardY, 0);
}, [screenHeight, keyboardVerticalOffset]);
const onLayoutWorklet = useCallback(
(layout: LayoutRectangle) => {
"worklet";
if (
keyboard.isClosed.value ||
initialFrame.value === null ||
behavior !== "height" // re-records on EVERY layout for "padding"
) {
initialFrame.value = layout;
}
},
[behavior],
);
Repo for reproducing
Not provided as a standalone repo. The snippet above reproduces it in a fresh app when behavior="padding", automaticOffset is on, and the wrapped block is content-sized (no flex: 1) and sits near the bottom of the screen. Happy to put together a minimal repo if needed.
To Reproduce
Steps to reproduce the behavior:
- Render a KeyboardAvoidingView with behavior="padding", automaticOffset, and a content-sized child (no flex: 1) near the bottom of the screen, containing a TextInput.
- Run on an affected Android device.
- Tap the TextInput to open the keyboard.
- See the content jitter/flicker up and down repeatedly before (sometimes) settling, rather than animating up smoothly once.
Expected behavior
The content should move up once, smoothly, and stay put — initialFrame should represent the resting frame and not be continuously re-recorded with the inflating geometry while the keyboard animates (as is already done for behavior="height").
Screenshots
If applicable, add screenshots to help explain your problem.
Smartphone (please complete the following information):
- Desktop OS: macOS 15.6 (Darwin 24.6.0)
- Device: <fill in — affected Android device, e.g. Samsung Galaxy S21>
- OS: <fill in — e.g. Android 14>
- RN version: 0.83.2
- RN architecture: new (Fabric)
- JS engine: Hermes
- Library version: 1.21.0
Additional context
- react-native-reanimated: 4.3.0
- The flicker disappears if the wrapped view is given a definite height (flex: 1), because the padding no longer changes the measured frame — but that shouldn't be required, and it doesn't fix automaticOffset's out-of-order measurements in the general case.
- Two directions for a fix:
a. Freeze initialFrame while the keyboard is open for self-measuring behaviors (padding, translate-with-padding), the same guard already applied to height. Only position animates a separate inner view and can safely keep refreshing. Concretely: keyboard.isClosed.value || initialFrame.value === null || behavior === "position".
b. Discard out-of-order automaticOffset measurements — only accept a layout whose bottom edge (y + height) is ≥ the stored one while the keyboard is opening, since the resting bottom edge only grows during open. This removes the late/stale async dips.
- Likely affects translate-with-padding for the same reason (paddingTop lands on the measured view). position is unaffected.
PR with Solution for this issue
#1515
Describe the bug
When using KeyboardAvoidingView with behavior="padding" (the same applies to any non-"height" behavior) and automaticOffset={true} on Android, the wrapped content jitters up and down repeatedly while the keyboard opens, instead of moving up smoothly once and settling. It is device-dependent (reproduces on some Android devices, not all).
Root cause appears to be in onLayoutWorklet. For any behavior other than "height", initialFrame is re-recorded on every onLayout:
if (
keyboard.isClosed.value ||
initialFrame.value === null ||
behavior !== "height" // <- always true for "padding"
) {
initialFrame.value = layout;
}
For behavior="padding", the animated paddingBottom is applied to the same view that carries onLayout. So when the view is content-sized:
This is a positive feedback loop. keyboardY is constant (screen height, heightWhenOpened, and keyboardVerticalOffset don't change), and the keyboard progress/height shared values are stable — I verified that the only oscillating inputs are frame.value.y / frame.value.height.
automaticOffset={true} amplifies it: each onLayout resolves the frame through an async KeyboardControllerNative.viewPositionInWindow() native call, and those promises settle out of order. A late callback delivers a stale (smaller) y + height after a newer larger one already landed, which yanks the recorded frame back down — producing the visible up/down flicker.
Measured initialFrame.value across a single keyboard open (logged inside onLayoutWorklet):
y 275, height 280 → y+height 555
y 253, height 314 → y+height 567
y 246, height 335 → y+height 581
y 225, height 364 → y+height 589
y 221, height 392.5 → y+height 613
y 217, height 396.5 → y+height 613 (settles)
height is supposed to be the constant resting height but ranges 280 → 396; y + height ramps 555 → 613 with out-of-order dips in between → that ramp + jitter is the flicker.
Code snippet
import { KeyboardAvoidingView } from "react-native-keyboard-controller";
import { TextInput, View } from "react-native";
// content-sized container (no flex:1), positioned toward the bottom of the screen
function Screen() {
return (
<View style={{ flex: 1, justifyContent: "flex-end" }}>
{/* content-sized block, NOT flex:1 */}
<View style={{ padding: 16, backgroundColor: "#ddd" }}>
<TextInput placeholder="focus me" style={{ height: 44 }} />
);
}
Relevant library code (src/components/KeyboardAvoidingView/index.tsx):
const relativeKeyboardHeight = useCallback(() => {
"worklet";
const keyboardY =
screenHeight - keyboard.heightWhenOpened.value - keyboardVerticalOffset;
return Math.max(frame.value.y + frame.value.height - keyboardY, 0);
}, [screenHeight, keyboardVerticalOffset]);
const onLayoutWorklet = useCallback(
(layout: LayoutRectangle) => {
"worklet";
if (
keyboard.isClosed.value ||
initialFrame.value === null ||
behavior !== "height" // re-records on EVERY layout for "padding"
) {
initialFrame.value = layout;
}
},
[behavior],
);
Repo for reproducing
Not provided as a standalone repo. The snippet above reproduces it in a fresh app when behavior="padding", automaticOffset is on, and the wrapped block is content-sized (no flex: 1) and sits near the bottom of the screen. Happy to put together a minimal repo if needed.
To Reproduce
Steps to reproduce the behavior:
Expected behavior
The content should move up once, smoothly, and stay put — initialFrame should represent the resting frame and not be continuously re-recorded with the inflating geometry while the keyboard animates (as is already done for behavior="height").
Screenshots
If applicable, add screenshots to help explain your problem.
Smartphone (please complete the following information):
Additional context
a. Freeze initialFrame while the keyboard is open for self-measuring behaviors (padding, translate-with-padding), the same guard already applied to height. Only position animates a separate inner view and can safely keep refreshing. Concretely: keyboard.isClosed.value || initialFrame.value === null || behavior === "position".
b. Discard out-of-order automaticOffset measurements — only accept a layout whose bottom edge (y + height) is ≥ the stored one while the keyboard is opening, since the resting bottom edge only grows during open. This removes the late/stale async dips.
PR with Solution for this issue
#1515