Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions docs/blog/2026-03-16-release-1-21/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,29 @@ Starting from `1.21.0`, `KeyboardAwareScrollView` uses `contentInset` on iOS and

This is a behavioral improvement — your existing `KeyboardAwareScrollView` usage needs no changes.

### Opting back into layout-based spacing with `mode="layout"`

There is, however, one class of UI where the old behavior was actually the _desired_ outcome: layouts that **intentionally rely on flex redistribution**. If you have a form where layout re-distribution helps to put form in correct position, the inset approach won't do that — it extends scroll space without touching layout.

For these cases, `1.21.0` ships a new `mode` prop:

```tsx
<KeyboardAwareScrollView
mode="layout"
contentContainerStyle={{ flex: 1, justifyContent: "space-between" }}
>
<FormFields />
<SubmitButton />
</KeyboardAwareScrollView>
```

| `mode` | How space is created | Layout reflow |
| ---------------------- | ------------------------------------- | --------------------- |
| `"insets"` _(default)_ | `contentInset` / `ClippingScrollView` | ❌ none |
| `"layout"` | Spacer `View` appended as last child | ✅ flex redistributes |

I intentionally made `"insets"` default to deliver best possible performance, but if your layout depends on layout changes - you may want to use `mode="layout"`.

## Other notable changes

Beyond the headline features, this release includes a couple of smaller but important additions.
Expand Down
28 changes: 28 additions & 0 deletions docs/docs/api/components/keyboard-aware-scroll-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,34 @@ If you have _**sticky elements**_ above the keyboard and want to extend the keyb
This property acts as [extraScrollHeight](https://github.com/APSL/react-native-keyboard-aware-scroll-view/tree/9eee405f7b3e261faf86a0fc8e495288d91c853e?tab=readme-ov-file#props) from original [react-native-keyboard-aware-scroll-view](https://github.com/APSL/react-native-keyboard-aware-scroll-view) package.
:::

### `mode`

Controls how keyboard space is created at the bottom of the `ScrollView`. Default is `"insets"`.

| Value | Behavior |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `"insets"` | Extends the scrollable area via `contentInset` (iOS) and `ClippingScrollView` (Android). Content layout is never modified — no layout reflows occur during keyboard animation. **Recommended for most use cases.** |
| `"layout"` | Appends a spacer `View` as the last child of the `ScrollView`. The spacer participates in layout, so flex-based arrangements (e.g. `justifyContent: "space-between"`, `gap`) reflow naturally when the keyboard appears. |

:::tip When to use `mode="layout"`?
If your `ScrollView` content relies on flex distribution — for example a form where a submit button should stay visually pinned to the bottom of the available space — use `mode="layout"` so the layout engine redistributes space when the keyboard appears.

```tsx
<KeyboardAwareScrollView
mode="layout"
contentContainerStyle={{ flex: 1, justifyContent: "space-between" }}
>
<FormFields />
<SubmitButton />
</KeyboardAwareScrollView>
```

:::

:::note
`mode="insets"` was introduced in `1.21.0` and is the default. `mode="layout"` restores the pre-`1.21.0` spacer-based behavior for cases where layout reflow is intentional.
:::

## Integration with 3rd party components

### `FlatList`/`FlashList`/`SectionList` etc.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,34 @@ If you have _**sticky elements**_ above the keyboard and want to extend the keyb
This property acts as [extraScrollHeight](https://github.com/APSL/react-native-keyboard-aware-scroll-view/tree/9eee405f7b3e261faf86a0fc8e495288d91c853e?tab=readme-ov-file#props) from original [react-native-keyboard-aware-scroll-view](https://github.com/APSL/react-native-keyboard-aware-scroll-view) package.
:::

### `mode`

Controls how keyboard space is created at the bottom of the `ScrollView`. Default is `"insets"`.

| Value | Behavior |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `"insets"` | Extends the scrollable area via `contentInset` (iOS) and `ClippingScrollView` (Android). Content layout is never modified — no layout reflows occur during keyboard animation. **Recommended for most use cases.** |
| `"layout"` | Appends a spacer `View` as the last child of the `ScrollView`. The spacer participates in layout, so flex-based arrangements (e.g. `justifyContent: "space-between"`, `gap`) reflow naturally when the keyboard appears. |

:::tip When to use `mode="layout"`?
If your `ScrollView` content relies on flex distribution — for example a form where a submit button should stay visually pinned to the bottom of the available space — use `mode="layout"` so the layout engine redistributes space when the keyboard appears.

```tsx
<KeyboardAwareScrollView
mode="layout"
contentContainerStyle={{ flex: 1, justifyContent: "space-between" }}
>
<FormFields />
<SubmitButton />
</KeyboardAwareScrollView>
```

:::

:::note
`mode="insets"` was introduced in `1.21.0` and is the default. `mode="layout"` restores the pre-`1.21.0` spacer-based behavior for cases where layout reflow is intentional.
:::

## Integration with 3rd party components

### `FlatList`/`FlashList`/`SectionList` etc.
Expand Down
108 changes: 79 additions & 29 deletions src/components/KeyboardAwareScrollView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Reanimated, {
scrollTo,
useAnimatedReaction,
useAnimatedRef,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
} from "react-native-reanimated";
Expand Down Expand Up @@ -104,6 +105,7 @@ const KeyboardAwareScrollView = forwardRef<
disableScrollOnKeyboardHide = false,
enabled = true,
extraKeyboardSpace = 0,
mode = "insets",
ScrollViewComponent = Reanimated.ScrollView,
snapToOffsets,
...rest
Expand Down Expand Up @@ -219,30 +221,39 @@ const KeyboardAwareScrollView = forwardRef<
},
[bottomOffset, enabled, height, snapToOffsets],
);
const removeGhostPadding = useCallback((e: number) => {
"worklet";
const removeGhostPadding = useCallback(
(e: number) => {
"worklet";

// new `ScrollViewWithBottomPadding` behavior: if we hide keyboard and we are in the end of `ScrollView`
// then we always need to scroll back, because we apply a padding that doesn't change layout, so we will
// not have auto scroll back in this case
if (!keyboardWillAppear.value && ghostViewSpace.value > 0) {
scrollTo(
scrollViewAnimatedRef,
0,
scrollPosition.value -
interpolate(
e,
[initialKeyboardSize.value, keyboardHeight.value],
[ghostViewSpace.value, 0],
),
false,
);
// layout mode: the spacer view participates in layout, so the ScrollView
// reflows naturally when it shrinks — no manual scroll correction needed.
if (mode === "layout") {
return false;
}

return true;
}
// insets mode: `ScrollViewWithBottomPadding` extends scrollable area without
// changing layout, so when the keyboard hides and we're at the end of the
// ScrollView we must manually scroll back.
if (!keyboardWillAppear.value && ghostViewSpace.value > 0) {
scrollTo(
scrollViewAnimatedRef,
0,
scrollPosition.value -
interpolate(
e,
[initialKeyboardSize.value, keyboardHeight.value],
[ghostViewSpace.value, 0],
),
false,
);

return false;
}, []);
return true;
}

return false;
},
[mode],
);
const performScrollWithPositionRestoration = useCallback(
(newPosition: number) => {
"worklet";
Expand Down Expand Up @@ -403,8 +414,14 @@ const KeyboardAwareScrollView = forwardRef<
scrollPosition.value = position.value;
// just persist height - later will be used in interpolation
keyboardHeight.value = e.height;
// and update keyboard spacer size
syncKeyboardFrame(e);

// insets mode: set the full contentInset upfront so that maybeScroll
// calculations are correct from the very first onMove frame.
// layout mode: do NOT set it here — the spacer must grow frame-by-frame
// in onMove to avoid a premature full-height jump before the keyboard moves.
if (mode === "insets") {
syncKeyboardFrame(e);
}
}

// focus was changed
Expand Down Expand Up @@ -445,13 +462,15 @@ const KeyboardAwareScrollView = forwardRef<
}
}

ghostViewSpace.value =
position.value +
scrollViewLayout.value.height -
scrollViewContentSize.value.height;
if (mode === "insets") {
ghostViewSpace.value =
position.value +
scrollViewLayout.value.height -
scrollViewContentSize.value.height;

if (ghostViewSpace.value > 0) {
scrollPosition.value = position.value;
if (ghostViewSpace.value > 0) {
scrollPosition.value = position.value;
}
}
},
onMove: (e) => {
Expand All @@ -461,6 +480,11 @@ const KeyboardAwareScrollView = forwardRef<
return;
}

// layout mode: drive the spacer view animation frame-by-frame
if (mode === "layout") {
syncKeyboardFrame(e);
}

// if the user has set disableScrollOnKeyboardHide, only auto-scroll when the keyboard opens
if (!disableScrollOnKeyboardHide || keyboardWillAppear.value) {
maybeScroll(e.height);
Expand Down Expand Up @@ -489,6 +513,7 @@ const KeyboardAwareScrollView = forwardRef<
},
},
[
mode,
maybeScroll,
removeGhostPadding,
disableScrollOnKeyboardHide,
Expand Down Expand Up @@ -555,6 +580,31 @@ const KeyboardAwareScrollView = forwardRef<
() => (enabled ? currentKeyboardFrameHeight.value : 0),
[enabled],
);
// layout mode only: a spacer view whose paddingBottom grows with the keyboard.
// The `+ 1` ensures the scroll view never reaches its absolute end during animation,
// avoiding the layout recalculation that triggers on every frame at the boundary.
// see: https://github.com/kirillzyusko/react-native-keyboard-controller/pull/342
const layoutSpacerStyle = useAnimatedStyle(
() =>
enabled && mode === "layout"
? { paddingBottom: currentKeyboardFrameHeight.value + 1 }
: {},
[enabled, mode],
);

if (mode === "layout") {
return (
<ScrollViewComponent
ref={onRef}
{...rest}
scrollEventThrottle={16}
onLayout={onScrollViewLayout}
>
{children}
{enabled && <Reanimated.View style={layoutSpacerStyle} />}
</ScrollViewComponent>
);
}

return (
<ScrollViewWithBottomPadding
Expand Down
11 changes: 11 additions & 0 deletions src/components/KeyboardAwareScrollView/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { AnimatedScrollViewComponent } from "../ScrollViewWithBottomPadding";
import type { ScrollView, ScrollViewProps } from "react-native";

export type KeyboardAwareScrollViewMode = "insets" | "layout";

export type KeyboardAwareScrollViewProps = {
/** The distance between the keyboard and the caret inside a focused `TextInput` when the keyboard is shown. Default is `0`. */
bottomOffset?: number;
Expand All @@ -10,6 +12,15 @@ export type KeyboardAwareScrollViewProps = {
enabled?: boolean;
/** Adjusting the bottom spacing of KeyboardAwareScrollView. Default is `0`. */
extraKeyboardSpace?: number;
/**
* Controls how keyboard space is created at the bottom of the `ScrollView`.
*
* - `"insets"` *(default)*: Extends the scrollable area via `contentInset` (iOS) and `ClippingScrollView` (Android). No layout reflow occurs — content positions remain stable during keyboard animation. Recommended for most use cases.
* - `"layout"`: Appends a spacer `View` as the last child of the `ScrollView`. The spacer participates in layout, so flex-based arrangements (e.g. `justifyContent: "space-between"`, `gap`) reflow naturally when the keyboard appears. Use this when you need content to physically rearrange around the keyboard space.
*
* Default is `"insets"`.
*/
mode?: KeyboardAwareScrollViewMode;
/** Custom component for `ScrollView`. Default is `ScrollView`. */
ScrollViewComponent?: AnimatedScrollViewComponent;
} & ScrollViewProps;
Expand Down
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export { default as KeyboardChatScrollView } from "./KeyboardChatScrollView";
export type { KeyboardAvoidingViewProps } from "./KeyboardAvoidingView";
export type { KeyboardStickyViewProps } from "./KeyboardStickyView";
export type {
KeyboardAwareScrollViewMode,
KeyboardAwareScrollViewProps,
KeyboardAwareScrollViewRef,
} from "./KeyboardAwareScrollView/types";
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type {
KeyboardChatScrollViewProps,
KeyboardAvoidingViewProps,
KeyboardStickyViewProps,
KeyboardAwareScrollViewMode,
KeyboardAwareScrollViewProps,
KeyboardAwareScrollViewRef,
KeyboardToolbarProps,
Expand Down
Loading