Mobile Keyboard Handling¶
Overview¶
How the Okta Mobile app manages on-screen keyboards across iOS and Android. Covers library choices, component APIs, per-screen patterns, and a checklist for new screens. Written for both human developers and AI assistants working in this codebase.
Architecture¶
Root-Level Setup¶
KeyboardProvider from react-native-keyboard-controller wraps the entire app in app/_layout.tsx:
// app/_layout.tsx (simplified)
import { KeyboardProvider } from "react-native-keyboard-controller";
export default function RootLayout() {
return (
<KeyboardProvider>
<I18nProvider>
<AuthProvider>
<TenantFeaturesProvider>
<Stack ... />
</TenantFeaturesProvider>
</AuthProvider>
</I18nProvider>
</KeyboardProvider>
);
}
This provider enables all react-native-keyboard-controller hooks and components throughout the app.
Android Layout Mode¶
In app.config.js, the Android keyboard layout mode is set to "pan":
"pan" scrolls the viewport up when the keyboard appears instead of resizing the layout. This avoids layout jumps and works well with KeyboardAwareScrollView and KeyboardAvoidingView.
Key Dependencies¶
| Package | Version | Purpose |
|---|---|---|
react-native-keyboard-controller |
^1.18.6 | KeyboardProvider, KeyboardAwareScrollView, KeyboardToolbar, useAnimatedKeyboard |
react-native-reanimated |
~3.16.1 | Animated values from useAnimatedKeyboard() for custom keyboard-tracking animations |
react-native-safe-area-context |
4.12.0 | Safe area insets used alongside keyboard offsets |
Screen Type Decision Matrix¶
| Screen Type | Component | Key Props | Example Files |
|---|---|---|---|
| Form (few fields, both platforms) | PlatformKeyboardScroll |
default props handle it | login.tsx, login-clinics.tsx, email-entry.tsx |
| Form (iOS only, needs field-to-field nav) | KeyboardAwareScrollView + KeyboardToolbar |
keyboardDismissMode="on-drag", keyboardShouldPersistTaps="handled", bottomOffset={100} |
sign-up.tsx, ForgotPasswordModal.tsx |
| Multi-step form with dropdowns | KeyboardAwareScrollView + KeyboardToolbar |
same as above | follow-up.tsx |
| Multi-line input form | KeyboardAwareScrollView + KeyboardToolbar |
same as above | first-question.tsx |
| Chat / message list | KeyboardAvoidingView + FlatList |
behavior="padding" (iOS), keyboardShouldPersistTaps="handled" |
conversation/[id].tsx |
| Search + list | FlatList (no scroll wrapper needed) |
keyboardShouldPersistTaps="handled", keyboardDismissMode="on-drag" |
exercise-search.tsx, patient picker |
| Modal with input | KeyboardAwareScrollView inside Modal |
keyboardDismissMode="on-drag", keyboardShouldPersistTaps="handled" |
ForgotPasswordModal.tsx |
| Screen with animated keyboard tracking | useAnimatedKeyboard() + Reanimated styles |
n/a (hook-based) | PatientFeedbackModal.tsx, CustomTabBar.tsx |
Component Reference¶
KeyboardAwareScrollView¶
From react-native-keyboard-controller. Automatically scrolls to keep the focused TextInput visible above the keyboard.
import { KeyboardAwareScrollView, KeyboardToolbar } from "react-native-keyboard-controller";
// Usage (from sign-up.tsx / follow-up.tsx pattern)
<KeyboardAwareScrollView
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
bottomOffset={100}
>
{/* form fields */}
</KeyboardAwareScrollView>
<KeyboardToolbar />
Props used in this codebase:
| Prop | Value | Purpose |
|---|---|---|
keyboardDismissMode |
"on-drag" |
Dismiss keyboard when user drags the scroll view |
keyboardShouldPersistTaps |
"handled" |
Allow button taps without first dismissing the keyboard |
bottomOffset |
100 |
Extra padding below focused input so it's not flush against keyboard |
KeyboardToolbar¶
From react-native-keyboard-controller. Renders a toolbar above the keyboard with previous/next/done buttons for field-to-field navigation.
import { KeyboardToolbar } from "react-native-keyboard-controller";
// Render outside the scroll view, at the bottom of the screen
<>
<KeyboardAwareScrollView>{/* ... */}</KeyboardAwareScrollView>
<KeyboardToolbar />
</>
Used in: login.tsx, login-clinics.tsx, email-entry.tsx, sign-up.tsx, first-question.tsx, follow-up.tsx, ForgotPasswordModal.tsx
PlatformKeyboardScroll¶
Custom wrapper (components/PlatformKeyboardScroll.tsx) that picks the right strategy per platform:
- iOS:
KeyboardAwareScrollViewwithkeyboardDismissMode="on-drag",keyboardShouldPersistTaps="handled",bottomOffset={100} - Android:
KeyboardAvoidingView(behavior"position") wrapping aScrollViewwithkeyboardShouldPersistTaps="handled"
import { PlatformKeyboardScroll } from "@/components/PlatformKeyboardScroll";
<PlatformKeyboardScroll keyboardVerticalOffset={-100}>
{/* form content */}
</PlatformKeyboardScroll>
Used in: DeleteAccountModal.tsx, profile.tsx, clinical-assessment/index.tsx, view-plan.tsx, follow-up-questions.tsx
useAnimatedKeyboard()¶
From react-native-keyboard-controller. Returns a Reanimated shared value tracking keyboard height in real-time. Use when you need pixel-level keyboard tracking for custom animations.
import { useAnimatedKeyboard } from "react-native-keyboard-controller";
import Animated, { useAnimatedStyle } from "react-native-reanimated";
const keyboard = useAnimatedKeyboard();
// Example: hide tab bar when keyboard is open (from CustomTabBar.tsx)
const animatedStyle = useAnimatedStyle(() => ({
display: keyboard.height.value > 0 ? "none" : "flex",
}));
// Example: adjust bottom padding (from PatientFeedbackModal.tsx)
const keyboardStyle = useAnimatedStyle(() => ({
paddingBottom: keyboard.height.value > 0 ? keyboard.height.value : 20,
}));
KeyboardAvoidingView¶
React Native built-in. Used for the chat screen where a FlatList needs to shrink when the keyboard opens rather than scroll.
// From conversation/[id].tsx
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : undefined}
keyboardVerticalOffset={headerHeight}
>
<FlatList
keyboardShouldPersistTaps="handled"
{/* messages */}
/>
<MessageInput />
</KeyboardAvoidingView>
TextInput Configuration Reference¶
Props used across the codebase for keyboard behavior:
| Prop | Values Used | Purpose | Example |
|---|---|---|---|
keyboardType |
"email-address", "phone-pad", "default" |
Controls which keyboard layout appears | login.tsx email field |
autoComplete |
"email", "current-password" |
Enables autofill suggestions | login.tsx, email-entry.tsx |
autoCapitalize |
"none", "sentences" (default) |
Controls auto-capitalization | "none" for email/password fields |
secureTextEntry |
true |
Masks password input | Password fields in login.tsx, sign-up.tsx |
multiline |
true |
Allows multi-line input | first-question.tsx, follow-up.tsx, MessageInput.tsx |
numberOfLines |
4, 6 |
Suggests visible lines for multiline | first-question.tsx (6), follow-up.tsx (4) |
textAlignVertical |
"top" |
Aligns text to top in multiline | All multiline TextInputs |
maxLength |
2000 |
Limits character count | MessageInput.tsx |
autoFocus |
true |
Opens keyboard immediately on mount | exercise-search.tsx |
Cross-Platform Considerations¶
Why PlatformKeyboardScroll Exists¶
KeyboardAwareScrollView from react-native-keyboard-controller works best on iOS but can behave unexpectedly on Android when combined with softwareKeyboardLayoutMode: "pan". The PlatformKeyboardScroll component abstracts this:
- iOS: Uses
KeyboardAwareScrollViewwhich natively tracks keyboard frames and scrolls smoothly to focused inputs. - Android: Uses
KeyboardAvoidingViewwithbehavior="position"which works well with the"pan"layout mode, pushing content up without resizing.
softwareKeyboardLayoutMode: "pan" vs "resize"¶
| Mode | Behavior | Trade-off |
|---|---|---|
"pan" (current) |
Viewport slides up, layout stays the same size | Works well with scroll-based keyboard avoidance; no layout reflows |
"resize" |
Layout height shrinks to fit above keyboard | Can cause layout jumps; interferes with absolute positioning |
The app uses "pan" because it provides smoother UX with the scroll-view-based keyboard management strategy.
Platform-Specific Prop Behavior¶
| Prop | iOS | Android |
|---|---|---|
keyboardDismissMode="on-drag" |
Works on ScrollView, FlatList | Works on ScrollView, FlatList |
keyboardShouldPersistTaps="handled" |
Works on ScrollView, FlatList | Works on ScrollView, FlatList |
KeyboardAvoidingView behavior |
Use "padding" |
Use undefined or "position" |
Common Patterns¶
Standard Form Screen¶
import { KeyboardAwareScrollView, KeyboardToolbar } from "react-native-keyboard-controller";
export default function FormScreen() {
return (
<SafeAreaView style={{ flex: 1 }}>
<KeyboardAwareScrollView
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
bottomOffset={100}
>
<TextInput placeholder="Name" />
<TextInput placeholder="Email" keyboardType="email-address" autoCapitalize="none" />
<TouchableOpacity onPress={handleSubmit}>
<Text>Submit</Text>
</TouchableOpacity>
</KeyboardAwareScrollView>
<KeyboardToolbar />
</SafeAreaView>
);
}
Cross-Platform Form (with PlatformKeyboardScroll)¶
import { PlatformKeyboardScroll } from "@/components/PlatformKeyboardScroll";
export default function CrossPlatformForm() {
return (
<SafeAreaView style={{ flex: 1 }}>
<PlatformKeyboardScroll>
<TextInput placeholder="Field" />
<TouchableOpacity onPress={handleSubmit}>
<Text>Submit</Text>
</TouchableOpacity>
</PlatformKeyboardScroll>
</SafeAreaView>
);
}
Chat Screen¶
import { KeyboardAvoidingView, Platform, FlatList } from "react-native";
export default function ChatScreen() {
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === "ios" ? "padding" : undefined}
keyboardVerticalOffset={headerHeight}
>
<FlatList
data={messages}
inverted
keyboardShouldPersistTaps="handled"
renderItem={({ item }) => <MessageBubble message={item} />}
/>
<MessageInput />
</KeyboardAvoidingView>
);
}
Modal with Input¶
import { KeyboardAwareScrollView, KeyboardToolbar } from "react-native-keyboard-controller";
<Modal visible={visible} presentationStyle="pageSheet">
<KeyboardAwareScrollView
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
bottomOffset={100}
>
<TextInput placeholder="Enter value" />
<Pressable onPress={handleSubmit}><Text>Done</Text></Pressable>
</KeyboardAwareScrollView>
<KeyboardToolbar />
</Modal>
Search with List¶
<View style={{ flex: 1 }}>
<TextInput
placeholder="Search..."
value={query}
onChangeText={setQuery}
/>
<FlatList
data={filtered}
keyboardShouldPersistTaps="handled"
keyboardDismissMode="on-drag"
renderItem={({ item }) => (
<TouchableOpacity onPress={() => selectItem(item)}>
<Text>{item.name}</Text>
</TouchableOpacity>
)}
/>
</View>
Animated Keyboard Tracking¶
import { useAnimatedKeyboard } from "react-native-keyboard-controller";
import Animated, { useAnimatedStyle } from "react-native-reanimated";
function KeyboardAwareComponent() {
const keyboard = useAnimatedKeyboard();
const style = useAnimatedStyle(() => ({
paddingBottom: keyboard.height.value > 0 ? keyboard.height.value : 16,
}));
return (
<Animated.View style={style}>
<TextInput placeholder="Type here" />
</Animated.View>
);
}
Current Implementation Audit¶
What's Done Well¶
- Root KeyboardProvider in
app/_layout.tsxenables all keyboard-controller features globally - Android
"pan"mode configured inapp.config.jsavoids layout resize jumps PlatformKeyboardScrollabstracts iOS/Android differences for form screensKeyboardToolbarused on all multi-field forms for field-to-field navigationkeyboardDismissMode="on-drag"on all scroll-based form screenskeyboardShouldPersistTaps="handled"on forms and lists so buttons work without double-tapuseAnimatedKeyboard()hides the tab bar when keyboard is open (no floating bar over the keyboard)useAnimatedKeyboard()adjusts padding inPatientFeedbackModalfor keyboard-aware layout- Chat screen uses
KeyboardAvoidingViewwith platform-specific behavior correctly
Remaining Gaps¶
| Gap | File(s) | Priority | Notes |
|---|---|---|---|
No returnKeyType on single-line fields |
login.tsx, email-entry.tsx, sign-up.tsx |
Low | Adding returnKeyType="next" / "done" improves keyboard UX but is cosmetic |
No field-to-field focus via ref + onSubmitEditing |
Multi-field forms | Low | KeyboardToolbar already provides prev/next buttons, so manual ref chaining is optional |
No Keyboard.dismiss() on background taps |
Various | Low | keyboardDismissMode="on-drag" covers most cases; explicit dismiss on background tap is a nice-to-have |
MessageInput.tsx has no returnKeyType="send" |
components/messaging/MessageInput.tsx |
Low | Would let users send via keyboard return key on single-line mode |
New Screen Checklist¶
When building a new screen that includes TextInput:
- [ ] Does the screen scroll? Use
KeyboardAwareScrollView(iOS-only) orPlatformKeyboardScroll(cross-platform) - [ ] Multiple text fields? Add
<KeyboardToolbar />after the scroll view for prev/next/done navigation - [ ] Has a submit button? Add
keyboardShouldPersistTaps="handled"so it works without dismissing keyboard first - [ ] Should keyboard dismiss on scroll? Add
keyboardDismissMode="on-drag" - [ ] Search + FlatList? Add
keyboardShouldPersistTaps="handled"andkeyboardDismissMode="on-drag"to the FlatList - [ ] Chat-style (input at bottom)? Use
KeyboardAvoidingViewwithbehavior="padding"(iOS) /undefined(Android) - [ ] Modal with input? Use
KeyboardAwareScrollViewinside the Modal; test on both platforms - [ ] Custom keyboard-tracking animation? Use
useAnimatedKeyboard()fromreact-native-keyboard-controller - [ ] Email field? Set
keyboardType="email-address",autoCapitalize="none",autoComplete="email" - [ ] Password field? Set
secureTextEntry,autoCapitalize="none",autoComplete="current-password" - [ ] Multiline? Set
textAlignVertical="top"for consistent cross-platform appearance