Setup in four steps
npx expo install react-native-keyboard-controller react-native-reanimated
npx expo run:ios # or run:android, or an EAS development buildIt is a native module, so after installing you need a fresh development build. Then wrap your root layout once:
// app/_layout.tsx
import { Stack } from 'expo-router';
import { KeyboardProvider } from 'react-native-keyboard-controller';
export default function RootLayout() {
return (
<KeyboardProvider>
<Stack />
</KeyboardProvider>
);
}Forgetting KeyboardProvideris the number one reason the library “does nothing”. Every component and hook reads keyboard state from it. Running an old build that predates the install is number two.
The components and what each is for
| Export | Use it for |
|---|---|
KeyboardAvoidingView | Drop-in replacement for the core one, same props, consistent on both platforms |
KeyboardAwareScrollView | Long forms: scrolls the focused input above the keyboard |
KeyboardStickyView | Chat composers and bottom bars that ride on top of the keyboard |
KeyboardToolbar | Previous / Next / Done bar above the keyboard for multi-field forms |
useKeyboardHandler | Your own worklet callbacks on start, move, and end of the keyboard animation |
The quickest win is swapping the import on an existing screen: change KeyboardAvoidingView from react-nativeto the library's version. Same props, but it animates in step with the keyboard and behaves the same on both platforms, so the per-platform behavior switch most codebases carry can go.
Long forms: KeyboardAwareScrollView
import { TextInput } from 'react-native';
import { KeyboardAwareScrollView, KeyboardToolbar } from 'react-native-keyboard-controller';
export default function ProfileForm() {
return (
<>
<KeyboardAwareScrollView
bottomOffset={24}
contentContainerStyle={{ padding: 16, gap: 12 }}
keyboardShouldPersistTaps="handled"
>
<TextInput placeholder="Name" />
<TextInput placeholder="Email" keyboardType="email-address" />
<TextInput placeholder="Company" />
<TextInput placeholder="Bio" multiline />
</KeyboardAwareScrollView>
<KeyboardToolbar />
</>
);
}When an input gains focus, the scroll view moves it just above the keyboard, with bottomOffset as breathing room. That is the behaviour people try to fake with KeyboardAvoidingView plus ScrollView plus manual scrollTo calls. KeyboardToolbar adds previous, next, and done buttons, which matter on iOS number pads that have no return key.
Chat input: KeyboardStickyView
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { KeyboardStickyView } from 'react-native-keyboard-controller';
export function Composer() {
const { bottom } = useSafeAreaInsets();
return (
<KeyboardStickyView offset={{ closed: 0, opened: bottom }}>
<View style={{ paddingBottom: bottom, flexDirection: 'row', padding: 8 }}>
<TextInput style={{ flex: 1 }} placeholder="Message" />
<SendButton />
</View>
</KeyboardStickyView>
);
}The composer moves with the keyboard every frame, so there is no gap or overlap mid-animation. The offset.opened value cancels the bottom safe-area padding once the keyboard covers the home indicator, otherwise the input floats one inset too high. The safe area guide explains where that inset comes from. For the message list itself, see the chat app guide.
Custom animation synced to the keyboard
import Animated, { useAnimatedStyle, interpolate } from 'react-native-reanimated';
import { useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller';
export function ShrinkingHeader() {
const { progress } = useReanimatedKeyboardAnimation(); // 0 closed, 1 open
const style = useAnimatedStyle(() => ({
height: interpolate(progress.value, [0, 1], [160, 64]),
opacity: interpolate(progress.value, [0, 1], [1, 0.6]),
}));
return <Animated.View style={style} />;
}progress and height are Reanimated shared values updated on the UI thread, so a header that shrinks while the keyboard rises stays perfectly in step. For full control over each phase, useKeyboardHandler gives you onStart, onMove, and onEnd worklets. The animations guide covers the Reanimated basics.
When the built-in component is enough
A login screen with two inputs does not need a native dependency. The core KeyboardAvoidingView handles it with the right behavior and offset. Reach for keyboard-controller when you have a composer pinned to the bottom, a form longer than one screen, inputs inside a bottom sheet, or Android and iOS disagreeing in ways you keep patching.
Keyboard bugs only show up on a real phone with a real keyboard. Describe the screen to ShipNative and it generates the React Native with keyboard handling included, then runs it on your phone, so you test the last field of the longest form before your users do.