The Keyboard

Keeping fields visible when the keyboard opens, dismissing it on tap, and moving focus from one field to the next.

Syntax<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

Example · tsx
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.

Example · tsx
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.

Example · tsx
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.

Example · tsx
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

  • Be the first to comment on this lesson.