React Native

Dismissing keyboard in React-Native: stop the struggle

When you create an application, you need to manage the keyboard and in theory, the principle is quite simple. When you press an input, the keyboard opens and when you press outside the input the keyboard closes. But this behavior is not trivial in React-Native and you will have to handle it by yourself.


Why is it not trivial?

I will distinguish between two types of screens here, screens wrapped with a View and a ScrollView.

If your screen is wrapped with a View, you can see below that the keyboard does not close automatically when you press outside your input.

If your screen is wrapped with a ScrollView, we have 3 different behaviors depending on the prop keyboardShouldPersistTaps and according to ScrollView documentation :

  • never (default) : Tapping outside of the focused text input when the keyboard is up dismisses the keyboard. When this happens, children won't receive the tap.
  • always :  The keyboard will not dismiss automatically, and the Scrollview will not catch taps, but children of the scroll view can catch taps.
  • handled : The keyboard will not dismiss automatically when the tap was handled by children of the scroll view (or captured by an ancestor).

What is missing here? It is impossible to handle button press and dismiss the keyboard with a single click. With never,  need one press to dismiss the keyboard and another to handle the button. With always, we simply cannot close the keyboard and with handled, pressing the button does not dismiss the keyboard.

The common keyboard dismisser solution

I first checked on the internet to see what the current solutions were and I found this : Wrapping your application with a Pressable that dismisses the keyboard. The Pressable that dismiss the keyboard. The accessible={false} is here to avoir breaking accessibility, you can check this article if you want to learn more.

<Pressable onPress={Keyboard.dismiss} accessible={false}>
<YourApp/>
</Pressable>

This is very simple but is equivalent to the keyboardShouldPersistTaps="handled" of the Scrollview, when we press a child button the keyboard is not dismissed and this can lead to bugs.

For example, if you want to open a BottomSheet when you click on a button by using presentation: "transparentModal" of react-navigation . You will have the following behavior on Android.

A better way to dismiss keyboard

This is how it works:

  • When you tap outside the keyboard, the component checks if the target is a text input.
  • At the end of the tap, it only dismiss the keyboard if the target is not a text input.
import type { PropsWithChildren } from "react";
import { useRef } from "react";
import { Keyboard, StyleSheet, View } from "react-native";
import * as TextInputState from "react-native/Libraries/Components/TextInput/TextInputState";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
export const KeyboardDismissPressable = ({ children }: PropsWithChildren) => {
const isTargetTextInput = useRef(false);
const tap = Gesture.Tap()
// Dismiss on tap end to avoid being triggered when scrolling
.onEnd(() => {
if (!isTargetTextInput.current) {
Keyboard.dismiss();
}
})
.runOnJS(true);
return (
<GestureDetector gesture={tap}>
<View
style={styles.container}
onStartShouldSetResponderCapture={(e) => {
// Allow to avoid keyboard flickering when clicking on a TextInput
isTargetTextInput.current = TextInputState.isTextInput(e.target);
return false;
}}
accessible={false}
>
{children}
</View>
</GestureDetector>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
});

Here we use react-native-gesture-handler to detect the tap and dismiss the keyboard even if a child (e.g. a button) catches it. Dismissing the keyboard at the end of the tap ensures that scrolling still works with the keyboard open. We also ensure that the keyboard is not dismissed when a TextInput is pressed with a solution inspired by  Scrollview’s code using isTextInput(e.target). When a TextInput is mounted, it reference is added to a Set and isTextInput simply checks if the inputs Set contains the target.

⚠️ Warning

  • Your application must be wrapped with a GestureHandlerRootView
  • I recommend wrapping each screen with the KeyboardDismissPressable instead of wrapping the whole application to avoid some conflicts and to be more flexible if some pages need a different behavior.
  • There is an import not allowed by typescript for TextInputState because this state is not exposed by react-native. So this internal function may be changed in future versions of react-native.

I am open to your challenges or ideas to improve this component so feel free to contact me if you would like to discuss this topic.

Développeur mobile ?

Rejoins nos équipes