The Keyboard
Keeping fields visible when the keyboard opens, dismissing it on tap, and moving focus from one field to the next.
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} />The software keyboard covers the bottom of the screen. Nothing moves out of its way on its own, so a field near the bottom can end up hidden behind it. Three separate problems: making room, getting rid of it, and moving between fields.
Making room
KeyboardAvoidingView resizes or shifts its children when the keyboard appears. Its behavior prop takes 'height', 'position' or 'padding'. The docs recommend setting it on both platforms, and the common choice is 'padding' on iOS and 'height' on Android, because Android often already resizes the window itself. keyboardVerticalOffset is a number you add when something sits above the view, such as a header. This is not CSS: there is no automatic layout reflow, and the component only affects its own subtree.
Dismissing
Keyboard.dismiss() closes it and drops focus. Wrap the screen in a Pressable that calls it, or on a ScrollView set keyboardDismissMode="on-drag". Also set keyboardShouldPersistTaps="handled", otherwise the first tap on a button inside a scroll view is swallowed by the dismissal and your onPress never runs.
Moving on
returnKeyType="next" labels the return key. onSubmitEditing fires when it is pressed, and a ref to the next TextInput lets you call focus(). Add submitBehavior="submit" so the field submits without blurring and the keyboard stays up. submitBehavior replaces the deprecated blurOnSubmit.
Shorthand and longhand
onPress={Keyboard.dismiss} is the same as onPress={() => Keyboard.dismiss()}.
Example
import { useRef, useState } from "react";
import {
Keyboard,
KeyboardAvoidingView,
Platform,
Pressable,
StyleSheet,
Text,
TextInput,
View,
} from "react-native";
// On screen: two stacked fields. Pressing return on the email field jumps
// straight to the password field without the keyboard closing; tapping the
// empty space around the fields closes it.
export default function SignInForm() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const passwordRef = useRef<TextInput>(null); // null until the field mounts
return (
<KeyboardAvoidingView
style={styles.flex}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<Pressable style={styles.flex} onPress={Keyboard.dismiss} accessible={false}>
<View style={styles.form}>
<Text style={styles.label}>Email</Text>
<TextInput
style={styles.input}
value={email}
onChangeText={setEmail}
keyboardType="email-address"
autoCapitalize="none"
returnKeyType="next"
submitBehavior="submit" // fire onSubmitEditing without blurring
onSubmitEditing={() => passwordRef.current?.focus()}
/>
<Text style={styles.label}>Password</Text>
<TextInput
ref={passwordRef}
style={styles.input}
value={password}
onChangeText={setPassword}
secureTextEntry
returnKeyType="done"
/>
</View>
</Pressable>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
flex: { flex: 1 },
form: { padding: 16, gap: 8, justifyContent: "center", flex: 1 },
label: { fontSize: 13, fontWeight: "600" },
input: { borderWidth: 1, borderColor: "#c6c6c8", borderRadius: 8, padding: 10 },
});When to use it
- Keeping a password field visible on a short sign-in screen when the keyboard opens.
- Letting the user tap anywhere on the background to put the keyboard away.
- Chaining return-key focus through name, email and phone fields in order.
- Pinning a message composer directly above the keyboard in a chat screen.
More examples
A scrolling form that dismisses on drag
A form that scrolls, closes the keyboard when you drag, and whose Save button responds on the first tap even while the keyboard is open. Use a ScrollView like this for a form of a few known fields; a long or unbounded list of rows belongs in a FlatList instead.
import { useState } from "react";
import { Pressable, ScrollView, StyleSheet, Text, TextInput } from "react-native";
export default function AddressForm() {
const [line1, setLine1] = useState("");
const [city, setCity] = useState("");
return (
<ScrollView
style={styles.flex}
contentContainerStyle={styles.content}
keyboardDismissMode="on-drag" // swipe anywhere to close it
keyboardShouldPersistTaps="handled" // without this, the first tap on Save is eaten
>
<TextInput style={styles.input} value={line1} onChangeText={setLine1} placeholder="Street" />
<TextInput style={styles.input} value={city} onChangeText={setCity} placeholder="City" />
<Pressable style={styles.button} onPress={() => console.log({ line1, city })}>
<Text style={styles.buttonText}>Save</Text>
</Pressable>
</ScrollView>
);
}
const styles = StyleSheet.create({
flex: { flex: 1 },
content: { padding: 16, gap: 12 },
input: { borderWidth: 1, borderColor: "#c6c6c8", borderRadius: 8, padding: 10 },
button: { backgroundColor: "#0a84ff", borderRadius: 8, padding: 14, alignItems: "center" },
buttonText: { color: "#fff", fontWeight: "600" },
});Reacting to the keyboard yourself
A bar that lifts itself by the exact keyboard height. Reach for the listeners when KeyboardAvoidingView is not enough, such as a toolbar pinned above the keyboard, or when you need to hide something while typing. Keyboard.isVisible() and Keyboard.metrics() read the same information on demand.
import { useEffect, useState } from "react";
import { Keyboard, StyleSheet, Text, View } from "react-native";
export default function KeyboardWatcher() {
const [height, setHeight] = useState(0);
useEffect(() => {
// keyboardWillShow/Hide are iOS only; the Did* pair fires on both platforms.
const show = Keyboard.addListener("keyboardDidShow", (event) => {
setHeight(event.endCoordinates.height);
});
const hide = Keyboard.addListener("keyboardDidHide", () => setHeight(0));
// Always remove both, or the listeners pile up on every mount.
return () => {
show.remove();
hide.remove();
};
}, []);
return (
<View style={[styles.bar, { marginBottom: height }]}>
<Text>Keyboard height: {height}</Text>
</View>
);
}
const styles = StyleSheet.create({
bar: { padding: 12, backgroundColor: "#f2f2f7" },
});Offsetting for a header
An input pinned to the bottom that rises to sit on top of the keyboard rather than being pushed too far or not far enough. The symptom that sends people here is a gap between the keyboard and the field, or the field still partly hidden: both mean keyboardVerticalOffset is wrong.
import { useState } from "react";
import { KeyboardAvoidingView, Platform, StyleSheet, TextInput, View } from "react-native";
const HEADER_HEIGHT = 56;
export default function CommentComposer() {
const [text, setText] = useState("");
return (
<View style={styles.flex}>
<View style={styles.header} />
<KeyboardAvoidingView
style={styles.flex}
behavior={Platform.OS === "ios" ? "padding" : "height"}
// The view does not start at y=0, so tell it how far down it begins.
keyboardVerticalOffset={Platform.OS === "ios" ? HEADER_HEIGHT : 0}
>
<View style={styles.spacer} />
<TextInput
style={styles.input}
value={text}
onChangeText={setText}
placeholder="Add a comment"
multiline
/>
</KeyboardAvoidingView>
</View>
);
}
const styles = StyleSheet.create({
flex: { flex: 1 },
header: { height: HEADER_HEIGHT, backgroundColor: "#f2f2f7" },
spacer: { flex: 1 },
input: { borderTopWidth: 1, borderTopColor: "#c6c6c8", padding: 12, maxHeight: 120 },
});
Discussion