Skip to content

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":

// app.config.js
android: {
  softwareKeyboardLayoutMode: "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: KeyboardAwareScrollView with keyboardDismissMode="on-drag", keyboardShouldPersistTaps="handled", bottomOffset={100}
  • Android: KeyboardAvoidingView (behavior "position") wrapping a ScrollView with keyboardShouldPersistTaps="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 KeyboardAwareScrollView which natively tracks keyboard frames and scrolls smoothly to focused inputs.
  • Android: Uses KeyboardAvoidingView with behavior="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>
  );
}
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

  1. Root KeyboardProvider in app/_layout.tsx enables all keyboard-controller features globally
  2. Android "pan" mode configured in app.config.js avoids layout resize jumps
  3. PlatformKeyboardScroll abstracts iOS/Android differences for form screens
  4. KeyboardToolbar used on all multi-field forms for field-to-field navigation
  5. keyboardDismissMode="on-drag" on all scroll-based form screens
  6. keyboardShouldPersistTaps="handled" on forms and lists so buttons work without double-tap
  7. useAnimatedKeyboard() hides the tab bar when keyboard is open (no floating bar over the keyboard)
  8. useAnimatedKeyboard() adjusts padding in PatientFeedbackModal for keyboard-aware layout
  9. Chat screen uses KeyboardAvoidingView with 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) or PlatformKeyboardScroll (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" and keyboardDismissMode="on-drag" to the FlatList
  • [ ] Chat-style (input at bottom)? Use KeyboardAvoidingView with behavior="padding" (iOS) / undefined (Android)
  • [ ] Modal with input? Use KeyboardAwareScrollView inside the Modal; test on both platforms
  • [ ] Custom keyboard-tracking animation? Use useAnimatedKeyboard() from react-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