Skip to content

KeyboardAvoidingView behavior="padding" + automaticOffset flickers/jitters content on Android (initialFrame feedback loop) #1514

Description

@anshulsingh1210

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:

  1. paddingBottom grows the view → its measured height (and, via automaticOffset, its y) changes.
  2. onLayout fires with the larger frame → initialFrame is re-recorded.
  3. 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:

  1. Render a KeyboardAvoidingView with behavior="padding", automaticOffset, and a content-sized child (no flex: 1) near the bottom of the screen, containing a TextInput.
  2. Run on an affected Android device.
  3. Tap the TextInput to open the keyboard.
  4. 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

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions