Every React Native app with a form hits the same wall. You tap a field low on the screen, the keyboard
opens, and the field is gone behind it. The Save button too.

The usual fixes get you most of the way. KeyboardAvoidingView behaves differently on iOS and
Android. KeyboardAwareScrollView from
react-native-keyboard-controller
is much better, and it's what I build on. But once a real form screen has a footer button, a
previous / next toolbar, a modal sheet or a validation error, the edge cases start piling up.

I hit all of them while building a booking app, so I pulled the fix out into a small package:
@saulzayn/rn-keyboard-form. This post shows
the problems, what was behind each one, and how to use it.

The same form without the package (the keyboard covers the field and Save) and with it

What a form screen needs

  • The focused field scrolls above the keyboard, and above anything sitting on the keyboard.
  • A footer (Save, Continue, Send) stays at the bottom and rides on top of the keyboard while it's open, so you never have to close the keyboard to submit.
  • A toolbar on the keyboard with ↑ / ↓ / Done to move between fields.
  • The same thing inside an iOS modal sheet, an iPad form sheet and a bottom sheet.
  • Phones and tablets, portrait and landscape, and turning the device with the keyboard open.

Each of these is easy alone. Together they fight each other.

The problems I ran into

1. The footer hides the field you're typing in. If the Save button rides on the keyboard, the
scroll view has to know the footer is there too, or it scrolls the field to just above the keyboard,
right behind the button. So the footer's height has to be added to the scroll offset. And when the
footer changes size (an error banner appears in it), that offset changes as well.

2. Changing that offset at the wrong moment scrolls twice. Inside an Android Modal, the footer's
safe-area padding changes while it rides up with the keyboard. Updating the scroll offset during
the animation made the field jump up twice. The fix was to hold the offset while the keyboard is
opening or closing, and apply changes only once it has settled.

3. Safe-area insets lie inside an iOS modal sheet. useSafeAreaInsets() still reports the
home-bar inset inside a page sheet, where the real bottom inset is 0. The package measures it with a
hidden SafeAreaView instead, so the footer doesn't get a gap under it.

4. iPad sheets only move part of the way. In an iPad form or page sheet, iOS lifts the sheet only
part of the way when the keyboard opens. So "the keyboard is 300pt tall" isn't how much of your form
it covers. The footer and toolbar are moved by the overlap: how far the keyboard actually reaches
into the form, measured in window coordinates.

5. The rounded keyboard on iOS 26+. The new keyboard has rounded top corners, and your form shows
through around them. The toolbar has to lift a little, and a strip behind it has to reach under the
corners.

6. Content that changes under the focused field. An error appears above the field you're in, or a
switch adds three more fields. The field moves, but nothing scrolls. The package re-measures the
focused field when the content size changes and scrolls the rest of the way.

7. Jumping. A Reddit commenter spotted this one in my demo video: moving to the next field while an
error and a footer banner appeared caused three scrolls in under a second. Now layout changes are
checked within one or two frames and corrected without animation, so it reads as one scroll.

8. Bottom sheets. A sheet that lifts itself above the keyboard plus a KeyboardAwareScrollView
inside it scrolled twice, once for each. Sheets get their own component that only scrolls the focused
field into view once the sheet has moved. On Android a sheet is also its own window, so ↓ on the
toolbar jumped to a field behind the sheet until each form's fields were grouped.

Using it

Install it next to its peers (with Expo, use npx expo install for the last three):

yarn add @saulzayn/rn-keyboard-form react-native-keyboard-controller react-native-reanimated react-native-safe-area-context 

keyboard-controller is a native module, so you need a
development build, not Expo Go.

Wrap the app once

With Expo Router, in the root app/_layout.tsx:

import { Stack } from 'expo-router'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { KeyboardHost } from '@saulzayn/rn-keyboard-form'; export default function RootLayout() { return ( <SafeAreaProvider> <KeyboardHost> <Stack /> </KeyboardHost> </SafeAreaProvider> ); } 

A form screen with a Save button

import { Button, TextInput } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { FormScroll } from '@saulzayn/rn-keyboard-form'; export default function EditProfile() { return ( <FormScroll footer={ <SafeAreaView edges={['bottom']} style={{ padding: 16, backgroundColor: 'white' }}> <Button title="Save" onPress={save} /> </SafeAreaView> } > <TextInput placeholder="Name" /> <TextInput placeholder="Email" keyboardType="email-address" /> <TextInput placeholder="Bio" multiline /> </FormScroll> ); } 

That's it: the focused field stays above the keyboard and the Save button, Save rides on the
keyboard, and the ↑ / ↓ / Done toolbar is on by default (toolbar={false} turns it off).

A bottom sheet

Your sheet moves itself above the keyboard; SheetScroll scrolls the focused field into view inside
it and keeps the footer in sight:

<SheetScroll contentContainerStyle={{ padding: 16, gap: 12 }} footer={<Button title="Send" onPress={send} />} > <TextInput placeholder="Name" /> <TextInput placeholder="Message" multiline /> </SheetScroll> 

Forms in steps

Give FormScroll (or SheetScroll) a key per step and focus the next step's first field, and the
keyboard stays up between steps on iOS. On Android, removing the focused field always closes the
keyboard, so the example app waits for it to close and then opens it on the next field. The README
has the details.

Theming

Light and dark themes are built in, and you can change the toolbar colours, the Done label, spacing
and the gap above the focused field:

<KeyboardHost theme={mergeTheme(darkTheme, { toolbar: { tint: '#E5484D', doneLabel: 'OK' } })}> 

How it was tested

Every screen in the example app on a Pixel 7 Pro, an iPhone with the iOS 26+ rounded keyboard, iPad
and Android tablet, in portrait and landscape, turning the device with the keyboard open, in light
and dark mode. It works with keyboard-controller 1.21.6 and newer (Expo SDK 56, 57 and 58).

Two things are still on my list: a multiline field low on the screen first scrolls only to its caret
(that's keyboard-controller's behaviour), and right after switching an Android app between full screen
and a floating window, the first keyboard open can be off.

Try it

All the heavy lifting is done by
react-native-keyboard-controller; this
package is the form-screen layer on top. If your form breaks on a device or layout I haven't covered,
please open an issue with a screen recording, and I'll fix it.