diff --git a/docs/blog/2026-03-16-release-1-21/index.mdx b/docs/blog/2026-03-16-release-1-21/index.mdx
index 9bbb5ebfd0..4c65095555 100644
--- a/docs/blog/2026-03-16-release-1-21/index.mdx
+++ b/docs/blog/2026-03-16-release-1-21/index.mdx
@@ -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
+
+
+
+
+```
+
+| `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.
diff --git a/docs/docs/api/components/keyboard-aware-scroll-view.mdx b/docs/docs/api/components/keyboard-aware-scroll-view.mdx
index f2a4da4f53..3f21d20ab2 100644
--- a/docs/docs/api/components/keyboard-aware-scroll-view.mdx
+++ b/docs/docs/api/components/keyboard-aware-scroll-view.mdx
@@ -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
+
+
+
+
+```
+
+:::
+
+:::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.
diff --git a/docs/versioned_docs/version-1.21.0/api/components/keyboard-aware-scroll-view.mdx b/docs/versioned_docs/version-1.21.0/api/components/keyboard-aware-scroll-view.mdx
index f2a4da4f53..3f21d20ab2 100644
--- a/docs/versioned_docs/version-1.21.0/api/components/keyboard-aware-scroll-view.mdx
+++ b/docs/versioned_docs/version-1.21.0/api/components/keyboard-aware-scroll-view.mdx
@@ -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
+
+
+
+
+```
+
+:::
+
+:::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.
diff --git a/src/components/KeyboardAwareScrollView/index.tsx b/src/components/KeyboardAwareScrollView/index.tsx
index da5bf242a4..90586e84b3 100644
--- a/src/components/KeyboardAwareScrollView/index.tsx
+++ b/src/components/KeyboardAwareScrollView/index.tsx
@@ -12,6 +12,7 @@ import Reanimated, {
scrollTo,
useAnimatedReaction,
useAnimatedRef,
+ useAnimatedStyle,
useDerivedValue,
useSharedValue,
} from "react-native-reanimated";
@@ -104,6 +105,7 @@ const KeyboardAwareScrollView = forwardRef<
disableScrollOnKeyboardHide = false,
enabled = true,
extraKeyboardSpace = 0,
+ mode = "insets",
ScrollViewComponent = Reanimated.ScrollView,
snapToOffsets,
...rest
@@ -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";
@@ -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
@@ -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) => {
@@ -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);
@@ -489,6 +513,7 @@ const KeyboardAwareScrollView = forwardRef<
},
},
[
+ mode,
maybeScroll,
removeGhostPadding,
disableScrollOnKeyboardHide,
@@ -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 (
+
+ {children}
+ {enabled && }
+
+ );
+ }
return (