Repository navigation
iOS: Sticky content lags/jumps when interactive keyboard dismissal gesture is started #1563
Description
Describe the bug
When using the KeyboardGestureArea, in the beginning of interactive keyboard dismissal, when the user is starting to drag down the keyboard, the sticky content in KeyboardStickyView lags behind/stutters. After the user is continuing to pan the keyboard down (or up), the sticky content jumps to the new position.
Code snippet
Also reproducible without KeyboardChatScrollView
function MySimplifiedChatScreen() {
const safeAreaPaddings = useSafeAreaPaddings();
const [composerHeight, setComposerHeight] = useState<number>(CONST.CHAT_FOOTER_MIN_HEIGHT);
const onComposerLayout = (e: LayoutChangeEvent) => setComposerHeight(e.nativeEvent.layout.height);
const keyboardGestureAreaOffset = composerHeight - safeAreaPaddings.paddingBottom;
return (
<KeyboardGestureArea
style={styles.flex1}
offset={keyboardGestureAreaOffset}
interpolator="ios"
textInputNativeID={CONST.COMPOSER.NATIVE_ID}
enableSwipeToDismiss={!isComposerFullSize}
>
{/* ... */}
<FlashList
renderScrollComponent={renderScrollComponent}
data={dummyData}
keyExtractor={(item) => item.id}
renderItem={({item}) => <Text>{item.name}</Text>}
/>
<KeyboardStickyView
onLayout={onComposerLayout}
offset={{closed: 0, opened: safeAreaPaddings.paddingBottom}}
>
<FooterWithInput />
</KeyboardStickyView>
{/* ... */}
</KeyboardGestureArea>
)
}
function renderScrollComponent({children, ...restProps}: ScrollViewProps) {
const safeAreaPaddings = useSafeAreaPaddings();
return (
<KeyboardChatScrollView
{...restProps}
offset={safeAreaPaddings.paddingBottom}
keyboardDismissMode="interactive"
>
{children}
</KeyboardChatScrollView>
);
}Repo for reproducing
Reproducible in example app:
ScreenRecording_07-28-2026.13-56-52_1.mov
To Reproduce
Steps to reproduce the behavior:
- Open the app and login
- (Create and) open a chat
- Open the keyboard by tapping the input -> the keyboard opens, the footer is pushed up on top of the keyboard
- Start do dismiss the keyboard fast by dragging down the chat, while continuing to hold the finger on the screen.
Expected behavior
The sticky content (footer) stays "attached" to the keyboard and follows it's animation.
Actual behavior
The sticky content is still where the keyboard ended before the interactive keyboard dismissal gesture has started.
Screenshots
If applicable, add screenshots to help explain your problem.
Notice that on the second pan gesture, you can see the sticky content (footer) stuck for a couple of milliseconds.
ScreenRecording_07-21-2026.17-32-42_1.mov
Smartphone (please complete the following information):
- Desktop OS: macOS 26
- Device: iPhone 14 Pro/iOS simulator
- OS: iOS 26.5.2/iOS 26.0
- RN version: 0.85.2
- RN architecture: new arch
- JS engine: Hermes
- Library version: 1.22.1
Additional context
Add any other context about the problem here.
Activity
[-]iOS: Interactive keyboard dismissal lags/jumps when started[/-][+]iOS: Sticky content lags/jumps when interactive keyboard dismissal gesture is started[/+]Deachronic commentedon Oct 7, 2026
We see the same thing on a physical iPhone 18 Pro Max (iOS 27.0.1), and it also reproduces without
KeyboardStickyView/KeyboardGestureArea: just a view translated byuseKeyboardAnimation().heightabove a list withkeyboardDismissMode="interactive"(KeyboardProviderwithpreload={false}).Measurements (60 fps screen recording, 3 drags, keyboard fully open beforehand). "Frozen" means the keyboard's top edge had moved and the input bar had not:
The rest of the UI rendered every frame during these windows, so it isn't a JS or main-thread stall. On the iOS 26.5 Simulator a synthesized drag tracks from the first frame. We could only reproduce it on the device.
Versions: observed on 1.21.9. We diffed the published 1.21.9 and 1.22.6 packages, and the iOS interactive path is byte-identical:
KeyboardMovementObserver+Interactive.swift,+Watcher,+Animation,+Lifecycle,KeyboardMovementObserver.swift,KeyboardViewLocator.swift,KeyboardEventsIgnorer.swiftandsrc/animated.tsx. So we expect 1.22.6 to behave the same, though we haven't run it on a device.Hypotheses (reasoned from source, not verified on the device):
keyboardDidMoveInteractivelyreturns early while!displayLink.isPaused, and on iOSuseKeyboardAnimationonly takesonKeyboardMoveStartandonKeyboardMoveInteractive. If the display link is still running when the drag begins, the first part of the drag would arrive only asonKeyboardMoveand be dropped. That fits "lags, then jumps".keyboardLayoutGuidetracking on iOS 26+,KeyboardTrackingView.interactive(point:)might returninvalidPositionfor the first KVO changes of a drag.A device log of the event names and heights for the first ~200 ms of a drag would tell these apart. We can try to collect one if that would help.
Environment: iPhone 18 Pro Max, iOS 27.0.1 (Simulator: iOS 26.5) · react-native-keyboard-controller 1.21.9 · React Native 0.86.3 (New Architecture) · Expo SDK 57 · Reanimated 4.5.1
Mingke commentedon Oct 17, 2026
A related data point, about the end of the interactive dismissal rather than its start. It may share a cause.
Setup
react-native-keyboard-controller1.22.7, New Architecture, iPhone 16 Pro Max, iOS 27.KeyboardGestureAreawithtextInputNativeIDandoffset = composerHeight + 11.useReanimatedKeyboardAnimation().height.keyboardDismissMode="interactive".What we measured (logging every change of
height.value)-364down to-offset(for example-112, -123, -128withoffset = 128).-offsetto0in a single frame. The composer drops byoffset + gap - restingBottom: about 97 pt with a two-line composer (offset128), and about 24 pt with a one-line composer (offset55).offset(364 + 128 = 492).Our reading of the source (please correct us)
KeyboardTrackingView.interactive(point:)). The invisible accessory view stays attached through the drag, so the value floors at the view's own height.KeyboardAreaExtender.hide(). That setsoffsetto 0 with no event and sets the ignore flag for a frame.keyboardWillHidethen emits a singleonKeyboardMoveStart(0), with no animation to inherit because the keyboard is already offscreen.We are working around it in the app by mapping the interactive range
[offset, keyboard]onto[0, keyboard]. An interactive tail emitted by the library would be the cleaner fix.