React Native Keyboard Handling in 2026: KeyboardAvoidingView vs react-native-keyboard-controller
Why react-native-keyboard-controller has replaced KeyboardAvoidingView for production React Native apps in 2026, with Expo setup, KeyboardAwareScrollView, sticky chat composers, the KeyboardToolbar, worklet animations, and the Android 15 edge-to-edge fix.
React Native keyboard handling in 2026 isn't a fight with KeyboardAvoidingView anymore. The modern answer is react-native-keyboard-controller, a JSI-powered library that gives you synchronized, frame-perfect keyboard animations on both iOS and Android with a single API. I've spent the last two years migrating production apps off the built-in component and onto Kirill Zyusko's library, and the difference in form usability, jank, and bug-report volume is night and day. So, let's walk through how to set it up with Expo, when each tool is the right choice, and the keyboard patterns that actually ship.
KeyboardAvoidingView still ships with React Native 0.83, but its padding, height, and position behaviors are platform-inconsistent and don't track keyboard frames in real time.
react-native-keyboard-controller 1.18+ provides a KeyboardAvoidingView drop-in plus KeyboardAwareScrollView, KeyboardToolbar, and reanimated values driven from the native UI thread.
The library works with Expo SDK 54+ via a config plugin, supports the New Architecture, and runs on iOS, Android, and React Native for Web.
For chat screens, use KeyboardStickyView to glue a composer to the keyboard. For forms, use KeyboardAwareScrollView to scroll focused inputs into view automatically.
Use useKeyboardHandler worklets when you need to animate alongside the keyboard at 120fps. They run on the UI thread, synchronized with the system keyboard animation.
Always set android:windowSoftInputMode="adjustResize" and disable edge-to-edge if you see overlap on Android 15. The controller relies on resize semantics.
Why KeyboardAvoidingView fails in real apps
The built-in KeyboardAvoidingView was designed for the simplest possible case: a single text input above the fold. The moment you add a header, a tab bar, an inset-aware safe area, or a multi-step form, the cracks show up. On iOS it works passably with behavior="padding". On Android the same code commonly does nothing, because Android's window resize already shifts your view and adding extra padding stacks on top. The official React Native docs even warn that the prop is platform-dependent and ask you to test on both platforms.
The deeper problem is timing. KeyboardAvoidingView listens to JS keyboardWillShow and keyboardDidShow events and animates with a JS-driven Animated timing. The native keyboard animation is driven by the system on the UI thread. Even on a fast device you get a 1–2 frame mismatch (it reads as a pop when the keyboard finishes opening). On Android the willShow event doesn't fire at all. You only get didShow after the animation completes, so the avoid-view jumps into place. None of these are bugs you can patch. They're architectural.
Honestly, I hit this exact bug shipping a fintech onboarding flow last year. Three weeks of iOS QA was green, then Android testers immediately flagged the keyboard "popping" up over the CTA button. react-native-keyboard-controller solves this by hooking the native keyboard animation callbacks (UIView.animate on iOS, WindowInsetsAnimationCallback on Android 11+) and forwarding the height and progress to JS, or directly to a Reanimated shared value on the UI thread. The result is pixel-locked tracking instead of an approximation, and identical behavior across both platforms with no per-OS branches in your code.
Install react-native-keyboard-controller in Expo
Add the library and its required peer dependencies. With Expo SDK 54 or newer you don't need to touch native code. The config plugin handles iOS pod installation and Android manifest edits during prebuild or EAS Build.
Register the plugin in your app.json (or app.config.ts). The plugin is optional, since installation alone works, but registering it lets the library configure android:windowSoftInputMode automatically and add the iOS keyboard background extension required for some sticky-view scenarios.
Wrap your app root once with KeyboardProvider. Every hook and component in the library reads its state from this provider, so it must sit above any screen that uses keyboard utilities.
import { KeyboardProvider } from "react-native-keyboard-controller";
export default function RootLayout() {
return (
<KeyboardProvider>
<Stack />
</KeyboardProvider>
);
}
Rebuild your dev client with npx expo run:ios and npx expo run:android, or trigger a new EAS development build. Reanimated 4 is already part of the New Architecture default in Expo SDK 54+, so no extra Babel config is needed beyond the standard babel-preset-expo plus react-native-reanimated/plugin. If you're still on the Old Architecture, the library falls back to a JS-only implementation that still beats KeyboardAvoidingView in correctness, but you lose the worklet integration.
Scroll focused inputs into view with KeyboardAwareScrollView
For most forms (sign-up, profile editing, address entry), what you actually want is "when an input is focused, scroll it above the keyboard with the right amount of breathing room." That's what KeyboardAwareScrollView does, and it does it by reading the focused input's frame and the live keyboard frame on the UI thread.
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
import { TextInput, Text } from "react-native";
export function SignUpForm() {
return (
<KeyboardAwareScrollView
bottomOffset={20}
contentContainerStyle={{ padding: 16, gap: 12 }}
keyboardShouldPersistTaps="handled"
>
<Text>Email</Text>
<TextInput keyboardType="email-address" autoCapitalize="none" />
<Text>Password</Text>
<TextInput secureTextEntry />
<Text>Confirm password</Text>
<TextInput secureTextEntry />
{/* 10 more fields below the fold; the scroll view handles them automatically */}
</KeyboardAwareScrollView>
);
}
The bottomOffset prop is how far above the keyboard the focused input should sit. 20 pixels is a sensible default, but for forms with helper text or validation messages bump it to 80 so the error line stays visible. Combined with type-safe forms built with React Hook Form and Zod, you get a sign-up flow where the focused field, its label, and any error never get hidden, on either platform, with zero per-screen tweaking.
If your form already lives inside a FlatList or FlashList, wrap it with the lower-level useReanimatedKeyboardAnimation hook instead, because KeyboardAwareScrollView doesn't virtualize. The hook gives you height and progress shared values you can plug into an animated padding style on the list's footer to keep the active row visible without losing virtualization.
Build a chat composer with KeyboardStickyView
Chat apps are the hardest keyboard scenario. The message composer must stick to the top of the keyboard while it animates open, the message list must slide up underneath, and pressing the list should dismiss the keyboard. KeyboardStickyView handles the first part in one component.
Two details matter. First, keyboardDismissMode="interactive" on the FlatList enables iOS-style "drag the list down to dismiss the keyboard," and the composer follows the keyboard frame as the user drags, courtesy of the controller library. Second, the useReanimatedKeyboardAnimation height is negative when the keyboard is open (it represents a Y-offset), so we invert it for the list padding. This pattern composes nicely with Gorhom's bottom sheet when you want a reply panel that slides up from the bottom and converts into a keyboard-anchored composer.
Add a Done toolbar with KeyboardToolbar
iOS users expect a "Done" button above the numeric keyboard. Android users expect a per-field "Next" action. KeyboardToolbar gives you both with one component, plus prev/next field traversal that respects focus order.
Drop it once at the root and it appears above every TextInput in the app. The toolbar walks the input tree to figure out which fields exist on the current screen, so "Next" jumps to the next visible input and "Done" hides the keyboard. You can pass showArrows={false} to keep only the Done button, or doneText="Submit" to relabel it for a final field. For teams doing serious form work, this single component eliminates the most-requested form-UX ticket on iOS without writing a line of custom focus management.
Animate alongside the keyboard with worklets
When you need a custom animation tied to the keyboard (say, fading a hero illustration as the keyboard opens, or shrinking a logo), use useKeyboardHandler. It registers a Reanimated worklet that runs on the UI thread for each frame of the keyboard animation, so your animation is frame-locked to the system.
Because useKeyboardHandler exposes the system-reported duration and easing curve, your custom animation matches the native keyboard's motion exactly. This is the same pattern documented in the official react-native-keyboard-controller documentation, and it ties into everything you already know from React Native Reanimated 4 worklets.
Fix Android 15 edge-to-edge overlap
Android 15 (API 35) made edge-to-edge mandatory for apps targeting it, which broke many keyboard layouts. If your composer or form sits beneath the keyboard on Android while iOS works fine, the cause is almost always that the system bars and IME insets aren't being applied to your view automatically.
The fix is two-fold. First, ensure the controller plugin set android:windowSoftInputMode="adjustResize" in your generated AndroidManifest.xml. Open the prebuild output and verify. Second, consume IME insets in your root layout using react-native-edge-to-edge together with KeyboardProvider.
import { SystemBars } from "react-native-edge-to-edge";
import { KeyboardProvider } from "react-native-keyboard-controller";
export default function RootLayout() {
return (
<KeyboardProvider statusBarTranslucent navigationBarTranslucent>
<SystemBars style="auto" />
<Stack />
</KeyboardProvider>
);
}
The statusBarTranslucent and navigationBarTranslucent props on KeyboardProvider tell the library to compensate for translucent system bars when computing keyboard insets. Without them, the keyboard appears to leave a gap on Android 15 devices with gesture navigation.
How do you dismiss the keyboard in React Native?
Three patterns cover 99% of dismissal needs in 2026. For tap-outside dismissal, wrap your form in Pressable with onPress={Keyboard.dismiss}. React Native's core Keyboard module still works exactly as before. For drag-to-dismiss in a scroll view, set keyboardDismissMode="interactive" on iOS or "on-drag" on Android; the controller normalizes both to "interactive" on platforms that support it. For programmatic dismissal (after a form submit, for example), call KeyboardController.dismiss(), which awaits the dismissal animation and resolves a Promise, unlike the fire-and-forget Keyboard.dismiss().
import { KeyboardController } from "react-native-keyboard-controller";
async function onSubmit(values) {
await KeyboardController.dismiss();
await api.signUp(values);
router.push("/welcome");
}
Awaiting dismissal before navigation prevents a common bug where the next screen mounts mid-animation and inherits a half-open keyboard frame, which on Android occasionally leaves the IME stuck open over the new screen.
KeyboardAvoidingView vs react-native-keyboard-controller
If you only remember one thing, use the built-in KeyboardAvoidingView for a quick prototype, and switch to the library the moment you ship to a customer. The table below summarizes the trade-offs.
Feature
KeyboardAvoidingView (built-in)
react-native-keyboard-controller 1.18+
iOS behavior
padding, height, or position (pick one)
Drop-in component + scroll view + sticky view
Android behavior
Effectively no-op with adjustResize
Full parity with iOS via WindowInsetsAnimationCallback
Frame-locked animation
No (JS-driven, 1–2 frames behind)
Yes (worklet on UI thread)
Drag-to-dismiss
Manual
keyboardDismissMode="interactive" cross-platform
Done toolbar
Build it yourself
<KeyboardToolbar />
New Architecture
Supported
Supported, with JSI fast path
Expo support
Native (no install)
npx expo install + config plugin
Bundle cost
0 KB
~28 KB gzipped JS + native module
The 28 KB JS cost and ~60 KB Android native footprint are real, but tiny next to the bug-report savings. The library is also actively maintained. The GitHub repo has shipped a release every two to three weeks throughout 2025 and into 2026, and the release notes on GitHub show consistent New Architecture and Android 15 fixes. For production apps, the maintenance velocity alone is worth the dependency.
Frequently Asked Questions
Does react-native-keyboard-controller work with Expo Go?
No. The library has a native module, so it requires a development build via npx expo run:ios/run:android or an EAS development build. Expo Go ships a fixed set of native modules and this is not one of them.
Why doesn't KeyboardAvoidingView work on Android?
Android already resizes your activity when android:windowSoftInputMode="adjustResize" is set, which is the default in modern React Native templates. Stacking KeyboardAvoidingView on top either doubles the offset or does nothing, because Android's keyboardWillShow event timing differs from iOS. react-native-keyboard-controller normalizes this by hooking the same WindowInsetsAnimation callback the system uses internally.
Is react-native-keyboard-controller compatible with the New Architecture?
Yes. Versions 1.10 and later target the New Architecture as the default and use JSI for frame-synchronous keyboard frame updates. On the Old Architecture, the library falls back to a pure-JS implementation that still works but loses the worklet-based UI thread integration.
How do I make the keyboard not cover the TextInput?
Wrap your screen in KeyboardAwareScrollView from react-native-keyboard-controller and set bottomOffset to the gap you want between the keyboard top and the focused input. 20 pixels is a sensible default, 80 if you display validation errors below the input.
Can I use react-native-keyboard-controller on React Native for Web?
Yes, since 1.12. The web build exposes the same hooks and components but resolves the keyboard height to zero (web browsers don't fire keyboard frame events), so animations no-op gracefully. This lets you share form code with an Expo Web target without conditional imports.
Ship real-time ML Kit face detection and OCR in an Expo development build using React Native Vision Camera v5 frame processors, runAsync, and worklets.
Ship iOS Live Activities and Dynamic Island from an Expo app: Swift widget extension, ActivityKit bridge, APNs push updates, and Android fallback patterns I've used in production.