rn-keyboard-controller
React Native Keyboard Controller — Best Practices Guide
Version 1.20.0 | react-native-reanimated required | Fabric & Paper supported
Table of Contents
| Section | Description |
|---|---|
| Critical Rules | Non-negotiable rules for correct keyboard handling |
| Quick Decision Guide | Which component/hook to use for your use case |
| Setup | Installation, provider, platform config |
| Hooks Overview | When and how to use each hook |
| Components Overview | When and how to use each component |
| Performance | 120fps animations, avoiding jank |
| Code Review Action | Audit keyboard code against best practices |
| References | Deep-dive files for APIs, patterns, troubleshooting |
Critical Rules
- Wrap your app with
<KeyboardProvider>— every hook and component depends on it. Place it above your navigation container. - Always add
"worklet"directive to everyuseKeyboardHandleranduseFocusedInputHandlercallback. Missing it causes silent failures or crashes. - Never use
useStateto track keyboard height during animations. State updates trigger re-renders on the JS thread. UseuseSharedValuefrom Reanimated instead — animations stay on the UI thread at 60-120fps. - Use
useKeyboardStatewith a selector to avoid unnecessary re-renders:useKeyboardState(s => s.isVisible)notuseKeyboardState(). - Use
KeyboardController.isVisible()in event handlers instead of reading fromuseKeyboardState— the static method doesn't cause re-renders. - Set
android:windowSoftInputMode="adjustResize"in AndroidManifest.xml (or equivalent in app.json). The library needs this on Android. - Install
react-native-reanimated— it's a mandatory peer dependency. The library will not work without it. - Don't put
<TextInput>inside<KeyboardExtender>— it won't work. UseKeyboardBackgroundView+KeyboardStickyViewcombo instead.
Quick Decision Guide
Pick the right tool for your use case:
| Use Case | Solution |
|---|---|
| Simple form with a few inputs | KeyboardAvoidingView with behavior="padding" |
| Scrollable form with many inputs | KeyboardAwareScrollView |
| Chat screen / messaging UI | useKeyboardHandler + Animated.View for input bar |
| Sticky input bar that follows keyboard | KeyboardStickyView |
| Form with prev/next/done navigation | KeyboardToolbar |
| Interactive swipe-to-dismiss keyboard | KeyboardGestureArea (Android 11+) + keyboardDismissMode="interactive" (iOS) |
| Content above keyboard (stickers, emoji) | OverKeyboardView |
| Extend keyboard with quick actions | KeyboardExtender |
| Match keyboard background color | KeyboardBackgroundView |
| Custom keyboard-driven animation | useKeyboardHandler (most powerful) |
| Check if keyboard is visible (no re-render) | KeyboardController.isVisible() |
| Check if keyboard is visible (reactive) | useKeyboardState(s => s.isVisible) |
| Dismiss keyboard programmatically | KeyboardController.dismiss() |
| Navigate between inputs | KeyboardController.setFocusTo("next"/"prev") |
Setup
Installation
# Expo (recommended)
npx expo install react-native-keyboard-controller
# Yarn / NPM
yarn add react-native-keyboard-controller
# Also install reanimated if not present
yarn add react-native-reanimated
Provider Setup
import { KeyboardProvider } from "react-native-keyboard-controller";
export default function App() {
return (
<KeyboardProvider>
{/* Navigation container and app content */}
</KeyboardProvider>
);
}
KeyboardProvider Props
| Prop | Type | Default | Purpose |
|---|---|---|---|
statusBarTranslucent |
boolean | true | StatusBar translucency on Android |
navigationBarTranslucent |
boolean | false | NavigationBar translucency on Android |
preserveEdgeToEdge |
boolean | false | Keep edge-to-edge when module disabled |
preload |
boolean | true | Preload keyboard to eliminate first-focus lag |
enabled |
boolean | true | Initial enabled state |
If the keyboard briefly flashes on app launch, disable preloading and call KeyboardController.preload() manually later.
Android Config
<!-- android/app/src/main/AndroidManifest.xml -->
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize">
</activity>
For Expo managed, use expo-build-properties plugin if you need to set Kotlin version.
iOS Config
For 120fps on ProMotion displays, add to Info.plist:
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
Hooks Overview
useKeyboardHandler — The Power Hook
The most flexible API. Gives you frame-by-frame control over keyboard animations via worklet callbacks.
import { useKeyboardHandler } from "react-native-keyboard-controller";
import { useSharedValue } from "react-native-reanimated";
const height = useSharedValue(0);
useKeyboardHandler({
onStart: (e) => { "worklet"; /* target values */ },
onMove: (e) => { "worklet"; height.value = e.height; },
onInteractive: (e) => { "worklet"; height.value = e.height; },
onEnd: (e) => { "worklet"; /* final values */ },
}, []);
onInteractivefires during gesture-driven dismissal (Android 11+ withKeyboardGestureArea)- Event data:
{ height, progress, duration, target, timestamp, type, appearance }
useKeyboardAnimation / useReanimatedKeyboardAnimation
Simpler hooks that return animated height and progress values. Use when you just need to track keyboard position without lifecycle control.
// Reanimated version (recommended)
const { height, progress } = useReanimatedKeyboardAnimation();
// height: SharedValue (0 → keyboard height in px)
// progress: SharedValue (0 → 1)
useKeyboardState
Reactive state that triggers re-renders. Always use with a selector:
const isVisible = useKeyboardState(s => s.isVisible);
const appearance = useKeyboardState(s => s.appearance);
useFocusedInputHandler
Track text changes and selection in the currently focused input from the UI thread:
useFocusedInputHandler({
onChangeText: ({ text }) => { "worklet"; /* ... */ },
onSelectionChange: ({ selection }) => { "worklet"; /* ... */ },
}, []);
useReanimatedFocusedInput
Get layout information about the focused input as a SharedValue:
const { input } = useReanimatedFocusedInput();
// input.value.layout.absoluteY, input.value.target, etc.
useKeyboardController
Toggle the library on/off at runtime:
const { enabled, setEnabled } = useKeyboardController();
Full hook API details → read
references/api-hooks.md
Components Overview
KeyboardAvoidingView
Drop-in replacement for RN's KeyboardAvoidingView. Consistent cross-platform behavior.
import { KeyboardAvoidingView } from "react-native-keyboard-controller";
<KeyboardAvoidingView behavior="padding" keyboardVerticalOffset={headerHeight}>
<TextInput />
</KeyboardAvoidingView>
Behaviors: "padding" (most common), "height", "position", "translate-with-padding" (best for chat).
KeyboardAwareScrollView
Auto-scrolls to keep focused inputs visible. Works with FlatList, FlashList, SectionList.
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
<KeyboardAwareScrollView bottomOffset={20}>
{/* Multiple TextInputs */}
</KeyboardAwareScrollView>
// With FlatList
<KeyboardAwareScrollView ScrollViewComponent={FlatList} data={data} renderItem={...} />
KeyboardStickyView
Translates a view to follow the keyboard without resizing the container.
import { KeyboardStickyView } from "react-native-keyboard-controller";
<KeyboardStickyView offset={{ closed: 0, opened: 20 }}>
<InputBar />
</KeyboardStickyView>
KeyboardToolbar
Pre-built toolbar with prev/next/done navigation. Uses compound component pattern.
import { KeyboardToolbar } from "react-native-keyboard-controller";
<KeyboardToolbar />
// Or customized
<KeyboardToolbar>
<KeyboardToolbar.Prev />
<KeyboardToolbar.Content><Text>Custom</Text></KeyboardToolbar.Content>
<KeyboardToolbar.Next />
<KeyboardToolbar.Done text="Close" />
</KeyboardToolbar>
Full component API details → read
references/api-components.md
Views Overview
| View | Purpose |
|---|---|
KeyboardGestureArea |
Interactive keyboard dismissal via gestures (Android 11+) |
OverKeyboardView |
Render content above keyboard without dismissing it |
KeyboardExtender |
Extend keyboard with custom UI (no TextInput inside!) |
KeyboardBackgroundView |
Match system keyboard background color |
Full views & core API details → read
references/api-views-core.md
Performance Rules
- All animation logic on the UI thread — use
useSharedValue+useAnimatedStyle, neveruseState - Add
"worklet"to every handler inuseKeyboardHandleranduseFocusedInputHandler - Use selectors with
useKeyboardState—useKeyboardState(s => s.isVisible)prevents full re-renders - Use
KeyboardController.isVisible()in press handlers instead of reactive state - Memoize list items with
React.memowhen using keyboard-aware lists - Enable 120fps on iOS via
CADisableMinimumFrameDurationOnPhonein Info.plist - Use
useKeyboardHandlerinstead ofKeyboardEventswhen you need animated responses — events fire on JS thread, handlers run on UI thread
Gradual Animation Pattern
The recommended pattern for smooth keyboard tracking (especially in chat UIs):
const useGradualAnimation = () => {
const height = useSharedValue(0);
useKeyboardHandler({
onMove: (e) => {
"worklet";
height.value = Math.max(e.height, 20); // minimum padding
},
onEnd: (e) => {
"worklet";
height.value = e.height;
},
}, []);
return { height };
};
Code Review & Audit Action
When asked to review, audit, or fix keyboard handling code:
- Scan all files importing from
react-native-keyboard-controller - Check against these rules:
| Check | Severity | What to look for |
|---|---|---|
| Missing KeyboardProvider | CRITICAL | App not wrapped in provider |
| Missing "worklet" directive | CRITICAL | useKeyboardHandler/useFocusedInputHandler callbacks without it |
| useState for keyboard height | HIGH | State updates during keyboard animation |
| useKeyboardState without selector | HIGH | Full object destructure causing extra re-renders |
| useKeyboardState in press handlers | HIGH | Should use KeyboardController.isVisible() |
| Missing adjustResize on Android | HIGH | windowSoftInputMode not set |
| TextInput inside KeyboardExtender | HIGH | Will not work — use KeyboardStickyView + KeyboardBackgroundView |
| Missing reanimated dependency | HIGH | Library requires react-native-reanimated |
| No 120fps iOS config | MEDIUM | Missing CADisableMinimumFrameDurationOnPhone |
| Using RN's KeyboardAvoidingView | MEDIUM | Should use library's version for cross-platform consistency |
| Missing keyboard preload handling | LOW | Keyboard may flash on app start |
- Report findings in a table with file, line, issue, severity, and fix
- Fix all issues if the user confirms
Platform Differences
| Behavior | iOS | Android |
|---|---|---|
| Frame-by-frame tracking | Via layout animation scheduling | Via WindowInsetsAnimationCallback |
| Interactive dismissal | keyboardDismissMode="interactive" on ScrollView |
KeyboardGestureArea (API 30+) |
| Progress values | Only 0 or 1 from events (use useKeyboardHandler for intermediate) |
Full intermediate values |
| Edge-to-edge | N/A | Library enables automatically |
| Soft input mode | N/A | adjustResize required, changeable at runtime |
Migration
From RN's KeyboardAvoidingView
// Before — platform-specific behavior prop
import { KeyboardAvoidingView } from "react-native";
<KeyboardAvoidingView behavior={Platform.OS === "ios" ? "padding" : undefined}>
// After — consistent cross-platform
import { KeyboardAvoidingView } from "react-native-keyboard-controller";
<KeyboardAvoidingView behavior="padding">
From react-native-keyboard-aware-scroll-view
// Before
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
// After — same API, better behavior
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
Reference Files
For deep dives, read these files in references/:
| File | Contents |
|---|---|
api-hooks.md |
Complete hooks API with all parameters, return types, edge cases |
api-components.md |
Component props, behaviors, integration patterns |
api-views-core.md |
Views (OverKeyboardView, KeyboardExtender, etc.) and core modules (KeyboardProvider, KeyboardController, KeyboardEvents) |
patterns-and-examples.md |
Real-world patterns: chat, forms, navigation, interactive dismissal, custom hooks |
troubleshooting.md |
Common errors, platform issues, compatibility matrix, Kotlin/Swift setup |