Build a form screen, tap a text field, and suddenly the keyboard slides up — covering the very input the user is trying to fill out. If you've been building apps with Rork, you've almost certainly hit this. What makes it especially frustrating is that iOS and Android behave differently, so a fix that works on one platform quietly breaks the other.
This guide walks through the root cause, then gives you working code for each scenario you're likely to encounter.
Why This Happens
React Native (the framework underlying Rork) doesn't automatically scroll content upward when the software keyboard appears. Unlike a mobile browser, the framework won't naturally resize or shift the layout to keep the focused input visible — you have to wire that up yourself.
The behavior also differs between platforms. iOS expects the padding behavior prop, while Android relies on the native windowSoftInputMode setting, which you can control in app.json for Expo managed projects.
First Check: softwareKeyboardLayoutMode in app.json
{
"expo": {
"android": {
"softwareKeyboardLayoutMode": "pan"
}
}
}Setting this to "pan" makes the entire screen slide up on Android when the keyboard appears. "resize" shrinks the content area instead. For most form screens, "pan" feels more natural.
One important caveat: this setting is Android-only. iOS ignores it entirely, so you'll need a separate solution for Apple devices.
The Standard Fix: KeyboardAvoidingView
import {
KeyboardAvoidingView,
Platform,
ScrollView,
TextInput,
StyleSheet,
} from 'react-native';
export default function ContactForm() {
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={Platform.OS === 'ios' ? 90 : 0}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
<TextInput
style={styles.input}
placeholder="Name"
returnKeyType="next"
/>
<TextInput
style={styles.input}
placeholder="Email address"
keyboardType="email-address"
returnKeyType="next"
/>
<TextInput
style={styles.input}
placeholder="Message"
multiline
numberOfLines={6}
textAlignVertical="top"
/>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
scrollContent: {
padding: 16,
paddingBottom: 40,
},
input: {
borderWidth: 1,
borderColor: '#ddd',
borderRadius: 8,
padding: 12,
marginBottom: 16,
fontSize: 16,
},
});Two things are critical here.
First, always branch the behavior prop with Platform.OS. iOS needs 'padding'; Android works better with 'height'. Leaving it undefined means iOS gets nothing.
Second, tune keyboardVerticalOffset to match your navigation bar height. With Expo Router's default stack header, 90 is usually close. If you're using a custom header, measure its actual height and adjust accordingly.
A Common Mistake: Forgetting the ScrollView
Adding KeyboardAvoidingView alone isn't enough if your form is tall. Without ScrollView inside it, inputs that scroll off the top of the screen are unreachable. Always pair them.
Also set keyboardShouldPersistTaps="handled" on the ScrollView. Without it, tapping outside a text field might not dismiss the keyboard, and taps on buttons inside the scroll area can get swallowed before they register.
For Chat-Style Layouts: useKeyboard Hook
When KeyboardAvoidingView doesn't behave (this happens frequently with bottom sheets, modals, and animated transitions), reading keyboard height directly gives you full control.
import { useEffect, useState } from 'react';
import { Keyboard, KeyboardEvent, Platform } from 'react-native';
export function useKeyboardHeight() {
const [keyboardHeight, setKeyboardHeight] = useState(0);
useEffect(() => {
const showEvent =
Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
const hideEvent =
Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
const showSub = Keyboard.addListener(
showEvent,
(e: KeyboardEvent) => {
setKeyboardHeight(e.endCoordinates.height);
}
);
const hideSub = Keyboard.addListener(hideEvent, () => {
setKeyboardHeight(0);
});
return () => {
showSub.remove();
hideSub.remove();
};
}, []);
return keyboardHeight;
}export default function ChatScreen() {
const keyboardHeight = useKeyboardHeight();
return (
<View style={{ flex: 1 }}>
<FlatList data={messages} /* ... */ />
<View
style={{
paddingBottom: keyboardHeight > 0 ? keyboardHeight : 16,
borderTopWidth: 1,
borderTopColor: '#eee',
padding: 8,
}}
>
<TextInput placeholder="Type a message..." />
</View>
</View>
);
}This pattern works well for chat UIs where the input bar is pinned to the bottom. The re-render on keyboard height change is lightweight enough that it won't cause performance issues in a typical chat screen.
Rork Max (SwiftUI): Different Rules Apply
If you're generating a native SwiftUI app with Rork Max, the React Native approach doesn't apply. In SwiftUI, use .ignoresSafeArea(.keyboard) with a ScrollView.
struct ContactFormView: View {
@State private var name = ""
@State private var email = ""
@State private var message = ""
var body: some View {
ScrollView {
VStack(spacing: 16) {
TextField("Name", text: $name)
.textFieldStyle(.roundedBorder)
TextField("Email", text: $email)
.textFieldStyle(.roundedBorder)
.keyboardType(.emailAddress)
TextEditor(text: $message)
.frame(height: 120)
.border(Color.gray.opacity(0.3))
}
.padding()
}
// Required when TextEditor is near the bottom of the screen
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}SwiftUI has built-in keyboard avoidance since iOS 15, but TextEditor specifically can still be obscured if the .ignoresSafeArea(.keyboard) modifier is missing. It's a small detail that's easy to overlook.
Tab Bar Screens Need Extra Offset
When your form is inside a tab-based navigation (Expo Router's Tabs, for example), add the tab bar height to keyboardVerticalOffset.
import { useSafeAreaInsets } from 'react-native-safe-area-context';
function FormInsideTabs() {
const insets = useSafeAreaInsets();
const TAB_BAR_HEIGHT = 49; // Expo Router default
return (
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
keyboardVerticalOffset={TAB_BAR_HEIGHT + insets.bottom}
>
{/* form content */}
</KeyboardAvoidingView>
);
}The keyboardVerticalOffset tells the component where to anchor its calculations. Leaving out the tab bar height causes the layout to shift by the wrong amount — sometimes barely noticeable, sometimes wildly off.
Choosing the Right Approach
- Simple forms (login, contact):
KeyboardAvoidingView+ScrollViewcovers most cases. - Chat UI or fixed bottom input: Use the
useKeyboardHeighthook for reliable control. - Rork Max (SwiftUI): Add
.ignoresSafeArea(.keyboard)and let SwiftUI handle the rest. - Screens inside a tab navigator: Include tab bar height in
keyboardVerticalOffset.
For related layout issues, see the Rork App Layout and Responsive Design Troubleshooting Guide. If you want to add input validation alongside these fixes, Form Validation with react-hook-form and Zod walks through that step by step.