Avoid Keyboard in React Native Like a Pro

Photo of Mateusz Mędrek

Mateusz Mędrek

Mar 2, 2022 • 67 min read
user typing on phone keyboard

When building functional and performant mobile UIs with different forms and inputs, developers have to fight with the common issue of soft keyboard covering the bottom parts of screens, which then are unreachable for the user, unless the keyboard is hidden.

In each project, developers have to incorporate text inputs into different types of forms, bottom sheets, or modals in a way that does not break those layouts when a soft keyboard is displayed. When writing React Native apps, additionally developers have to think of how to resolve it in a way that will result in consistent behavior on both platforms. There are a few solutions that can be leveraged, to achieve the best possible effect, beginning with built-in, through 3rd party, and ending with completely custom logic written for specific use cases. This article is supposed to describe and compare them in different test cases. But before that, let’s check how that keyboard trouble can be handled in native Android and iOS projects.

Android has a built-in android:windowSoftInputMode activity parameter, which controls how the keyboard is displayed together with the activity's UI. On iOS, there is no built-in way. Developers instead rely on some custom logic or the popular IQKeyboardManager library. These (native) solutions can be used in React Native project - setting value in the manifest file on Android and installing react-native-keyboard-manager for iOS, which is an RN wrapper for IQKeyboardManager.

So now, let's check, how it can be approached in React Native:

Avoid keyboard solutions

KeyboardAvoidingView + android:windowSoftInputMode=”adjustPan”

KeyboardAvoidingView is a React Native built-in component with full JS implementation. It relies on RN’s keyboard events (keyboardWillChangeFrame on iOS & keyboardDidHide/Show on Android) and, based on provided behavior prop, applies additional padding, translation, or changes container’s height.

react-native-keyboard-aware-scroll-view + android:windowSoftInputMode=”adjustPan”

react-native-keyboard-aware-scroll-view is a library with full JS implementation that provides an enhanced ScrollView component that reacts on keyboard events by applying bottom padding and scrolling to currently focused input. It also provides FlatList & SectionList implementation. It also exposes listenToKeyboardEvents HOC, which can be used to wrap custom scroll components.

react-native-keyboard-manager + android:windowSoftInputMode=”adjustResize”

react-native-keyboard-manager is a wrapper library for iOS IQKeyboardManager. It is a “go-to” solution used in Swift & Objective-C projects. It applies padding or translation to the correct containeralso provides methods for soft keyboard appearance customization.

react-native-avoid-softinput

react-native-avoid-softinput is a library with native implementation. It exposes native module which can globally react to keyboard events and apply padding or translation to react root view. It also contains native component that is intended to wrap content which should be pushed above the keyboard.

Test Cases

For comparison, following examples will be used:

  • Form example - screen with text inputs (single line & multiline) which have different height
  • Bottom sheet example with @gorhom/bottomsheet lib - screen with bottom sheet containing single text field and confirm button
  • Modal form example with RN Modal component - screen with modal containing text inputs (single line & multiline) which have different height
  • Bottom sheet modal example with RN Modal component - screen with modal containing single text field and confirm button
  • Portal form example with @gorhom/portal lib - screen with portal containing text inputs (single line & multiline) which have different height
  • Multiple inputs example - screen with multiple text fields put inside scrollable component

All examples are available in the test repo.

Form example

Let’s start with, probably, the most popular use case.


const FormExample: React.FC = () => {
  //...

  return <>
    <View style={styles.logoContainer}>
      <Image
        resizeMode="contain"
        source={ { uri: 'https://reactnative.dev/img/tiny_logo.png' } }
        style={styles.logo}
      />
    </View>
    <View style={styles.inputsContainer}>
      <TextInput
        placeholder="Single line input"
        style={styles.input}
      />
      <TextInput
        multiline
        placeholder="Multiline input"
        style={[ styles.input, styles.multilineInput ]}
      />
      <TextInput
        multiline
        placeholder="Large multiline input"
        style={[ 
styles.input,
styles.multilineInput,
styles.largeMultilineInput
]} /> </View> <View style={styles.submitButtonContainer}> <Button onPress={() => { // On submit ... }} title="Submit" /> </View> </>; };

Form component will be wrapped in a full-screen scroll component (defaults to ScrollView).

To consider the solution successful, it should handle:

  • applying bottom padding or translation
  • pushing text field above the keyboard, but below screen’s top edge

KeyboardAvoidingView + android:windowSoftInputMode=”adjustPan”


const KeyboardAvoidingViewFormScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustPan();
    return () => {
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);

  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <KeyboardAvoidingView behavior="position" style={styles.keyboardAvoidingView}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll} > <FormExample /> </ScrollView> </KeyboardAvoidingView> </SafeAreaView>; };

react-native-keyboard-aware-scroll-view + android:windowSoftInputMode=”adjustPan”


const KeyboardAwareScrollViewFormScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustPan();
    return () => {
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);
  
  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={commonStyles.screenContainer}> <KeyboardAwareScrollView enableOnAndroid={true} enableResetScrollToCoords={false} bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <FormExample /> </KeyboardAwareScrollView> </SafeAreaView>; };

react-native-keyboard-manager + android:windowSoftInputMode=”adjustResize”


const IQKeyboardManagerFormScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    if (Platform.OS !== 'ios') {
      AvoidSoftInput.setAdjustResize();
    } else {
      RNKeyboardManager.setEnable(true);
    }

    return () => {
      if (Platform.OS !== 'ios') {
        AvoidSoftInput.setDefaultAppSoftInputMode();
      } else {
        RNKeyboardManager.setEnable(false);
      }
    };
  }, []);

  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={commonStyles.screenContainer}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <FormExample /> </ScrollView> </SafeAreaView>; };

react-native-avoid-softinput - native module


const AvoidSoftInputModuleFormScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustNothing();
    AvoidSoftInput.setEnabled(true);
    return () => {
      AvoidSoftInput.setEnabled(false);
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);

  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={commonStyles.screenContainer}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <FormExample /> </ScrollView> </SafeAreaView>; };

react-native-avoid-softinput - native component


const AvoidSoftInputViewFormScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustNothing();
    return () => {
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);

  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={commonStyles.screenContainer}> <AvoidSoftInputView style={styles.avoidSoftInputView}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <FormExample /> </ScrollView> </AvoidSoftInputView> </SafeAreaView>; };

Results:

On a screen with KeyboardAvoidingView, when the keyboard pops up, the top and bottom parts of the screen are clipped, so the "Submit" button is not accessible. In addition, when multiline input is focused, it is pushed up even when it won't be covered by a soft keyboard.

In react-native-keyboard-aware-scroll-view screen on iOS, after selecting the first input, it is pushed above the keyboard. When selecting large input, it is positioned slightly above the top edge of the visible area, but the rest of the content (at the top and bottom of the screen) is easily accessible. On Android, the bottom part of the screen is slightly clipped, but when the keyboard is displayed, inputs are correctly pushed above and only if needed.

Screens with react-native-keyboard-manager and react-native-avoid-softinput are handling keyboard with similar effect - single line input's bottom edge is pushed above the keyboard only if needed, when focusing large input it is pushed only to the top of the ScrollView top edge. Whenever the keyboard is visible, the user can scroll to the very bottom of the screen.

Bottom sheet example

In the bottom sheet example, BottomSheetModal component from @gorhom/bottom-sheet library will be used. An example will accept BottomSheetWrapper prop to enable wrapping content that should be displayed above the keyboard (default wrapper is simple View component).


const SNAP_POINTS = [ 'CONTENT_HEIGHT' ];

const Backdrop: React.FC = () => <View style={styles.backdrop} />;

const DefaultBottomSheetWrapper: React.FC = ({ children }) => <View 
style={styles.container}>
{children}
</View>; interface Props { BottomSheetWrapper?: React.FC } const BottomSheetExample: React.FC<Props> = ({
BottomSheetWrapper = DefaultBottomSheetWrapper
}) => { //... return <View style={commonStyles.screenContainer}> <Button onPress={presentBottomSheet} title="Open bottom sheet" /> <BottomSheetModal ref={bottomSheetModalRef} backdropComponent={Backdrop} contentHeight={animatedContentHeight} enableDismissOnClose enablePanDownToClose handleHeight={animatedHandleHeight} index={0} snapPoints={animatedSnapPoints} > <BottomSheetView
onLayout={handleContentLayout}
style={styles.container}> <SafeAreaView edges={[ 'bottom' ]} style={styles.container}> <BottomSheetWrapper> <Text style={styles.header}>Bottom sheet header</Text> <TextInput style={styles.input} /> <View style={styles.buttonContainer}> <Button onPress={dismissBottomSheet} title="Confirm" /> </View> </BottomSheetWrapper> </SafeAreaView> </BottomSheetView> </BottomSheetModal> </View>; };

In this test case, there is no scroll component used, so react-native-keyboard-aware-scroll-view will not be checked.

To consider the solution successful, it should handle:

  • pushing whole bottom sheet content above the top edge of the keyboard

KeyboardAvoidingView + android:windowSoftInputMode=”adjustPan”


const BottomSheetWrapper: React.FC = ({ children }) => {
  return <KeyboardAvoidingView
    behavior="position"
    contentContainerStyle={styles.keyboardAvoidingView}
    style={styles.keyboardAvoidingView}>
    {children}
  </KeyboardAvoidingView>;
};

const KeyboardAvoidingViewBottomSheetScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustPan();
    return () => {
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);
  
  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={commonStyles.screenContainer}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll} > <BottomSheetExample BottomSheetWrapper={BottomSheetWrapper} /> </ScrollView> </SafeAreaView>; };

react-native-keyboard-manager + android:windowSoftInputMode=”adjustResize”


const IQKeyboardManagerBottomSheetScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    if (Platform.OS !== 'ios') {
      AvoidSoftInput.setAdjustResize();
    } else {
      RNKeyboardManager.setEnable(true);
      RNKeyboardManager.setKeyboardDistanceFromTextField(100);
    }

    return () => {
      if (Platform.OS !== 'ios') {
        AvoidSoftInput.setDefaultAppSoftInputMode();
      } else {
        RNKeyboardManager.setEnable(false);
// Default value RNKeyboardManager.setKeyboardDistanceFromTextField(10); } }; }, []); useFocusEffect(onFocusEffect); return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <BottomSheetExample /> </ScrollView> </SafeAreaView>; };

react-native-avoid-softinput - native module


const AvoidSoftInputModuleBottomSheetScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustNothing();
    AvoidSoftInput.setEnabled(true);
    AvoidSoftInput.setAvoidOffset(100);
    return () => {
      AvoidSoftInput.setEnabled(false);
      AvoidSoftInput.setDefaultAppSoftInputMode();
      AvoidSoftInput.setAvoidOffset(0); // Default value
    };
  }, []);

  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <BottomSheetExample /> </ScrollView> </SafeAreaView>; };

react-native-avoid-softinput - native component


const BottomSheetWrapper: React.FC = ({ children }) => {
  return <AvoidSoftInputView style={styles.avoidSoftInputView}>
    {children}
  </AvoidSoftInputView>;
};

const AvoidSoftInputViewBottomSheetScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustNothing();
    return () => {
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);

  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={commonStyles.screenContainer}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <BottomSheetExample BottomSheetWrapper={BottomSheetWrapper} /> </ScrollView> </SafeAreaView>; };

Results:

In KeyboardAvoidingView screen, after opening the bottom sheet and focusing input, content is not changing its position, which makes it covered by the keyboard.

In screens with react-native-keyboard-manager and react-native-avoid-softinput "module", when example text field is focused, the entire bottom sheet is pushed above the keyboard. When the bottom sheet is dismissed, the keyboard disappears and screen goes to the start position.

On screen with react-native-avoid-softinput "component", the bottom sheet stays on its position when the keyboard pops up, however, content is translated above the top edge of the keyboard which makes it clipped and invisible for the user.

Modal form example

This example will use core's React Native Modal component. An example will accept a ModalContentWrapper as a prop, which will then wrap content displayed in modal and apply translation or padding when the keyboard will show (default wrapper will use ScrollView).


const DefaultModalContentWrapper: React.FC = ({ children }) => <View
style={styles.wrapper}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll} > {children} </ScrollView> </View>; interface Props { ModalContentWrapper?: React.FC } const ModalFormExample: React.FC<Props> = ({
ModalContentWrapper = DefaultModalContentWrapper
}) => { //... return <View style={commonStyles.screenContainer}> <Button onPress={openModal} title="Open modal" /> <RNModal animationType="slide" onRequestClose={closeModal} statusBarTranslucent={true} style={styles.modal} supportedOrientations={[ 'landscape', 'portrait' ]} transparent={true} visible={isModalVisible} > <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={styles.modalContent}> <View style={styles.container}> <View style={styles.wrapper}> <CloseButton onPress={closeModal} /> </View> <ModalContentWrapper> <View style={styles.spacer} /> <View style={styles.inputsContainer}> <TextInput placeholder="Single line input" style={styles.input} /> <TextInput multiline placeholder="Multiline input" style={[ styles.input, styles.multilineInput ]} /> </View> </ModalContentWrapper> </View> </SafeAreaView> </RNModal> </View>; };

To consider the solution successful, it should handle:

  • applying bottom padding or translation
  • pushing text input above the keyboard

KeyboardAvoidingView + android:windowSoftInputMode=”adjustPan”


const KeyboardAvoidingViewModalContentWrapper: React.FC = ({
children
}) => <KeyboardAvoidingView behavior="position" contentContainerStyle={styles.keyboardAvoidingView} style={styles.keyboardAvoidingView}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll} > {children} </ScrollView> </KeyboardAvoidingView>; const KeyboardAvoidingViewModalScreen: React.FC = () => { const onFocusEffect = useCallback(() => { AvoidSoftInput.setAdjustPan(); return () => { AvoidSoftInput.setDefaultAppSoftInputMode(); }; }, []); useFocusEffect(onFocusEffect); return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <ModalFormExample ModalContentWrapper={KeyboardAvoidingViewModalContentWrapper} /> </SafeAreaView>; };

react-native-keyboard-aware-scroll-view + android:windowSoftInputMode=”adjustPan”


const KeyboardAwareScrollViewModalContentWrapper: React.FC = ({
children
}) => <KeyboardAwareScrollView enableOnAndroid={true} enableResetScrollToCoords={false} bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> {children} </KeyboardAwareScrollView>; const KeyboardAwareScrollViewModalScreen: React.FC = () => { const onFocusEffect = useCallback(() => { AvoidSoftInput.setAdjustPan(); return () => { AvoidSoftInput.setDefaultAppSoftInputMode(); }; }, []); useFocusEffect(onFocusEffect); return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <ModalFormExample ModalContentWrapper={KeyboardAwareScrollViewModalContentWrapper} /> </SafeAreaView>; };

react-native-keyboard-manager + android:windowSoftInputMode=”adjustResize”


const IQKeyboardManagerModalScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    if (Platform.OS !== 'ios') {
      AvoidSoftInput.setAdjustResize();
    } else {
      RNKeyboardManager.setEnable(true);
    }

    return () => {
      if (Platform.OS !== 'ios') {
        AvoidSoftInput.setDefaultAppSoftInputMode();
      } else {
        RNKeyboardManager.setEnable(false);
      }
    };
  }, []);
  
  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <ModalFormExample /> </SafeAreaView>; };

react-native-avoid-softinput - native module


const AvoidSoftInputModuleModalScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustNothing();
    return () => {
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);
  
  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <ModalFormExample /> </SafeAreaView>; };

react-native-avoid-softinput - native component


const AvoidSoftInputViewModalContentWrapper: React.FC = ({
children
}) => <AvoidSoftInputView style={styles.avoidSoftInput}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll} > {children} </ScrollView> </AvoidSoftInputView>; const AvoidSoftInputViewModalScreen: React.FC = () => { const onFocusEffect = useCallback(() => { AvoidSoftInput.setAdjustNothing(); return () => { AvoidSoftInput.setDefaultAppSoftInputMode(); }; }, []); useFocusEffect(onFocusEffect); return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <ModalFormExample ModalContentWrapper={AvoidSoftInputViewModalContentWrapper} /> </SafeAreaView>; };

Results:

On a screen with KeyboardAvoidingView, when input is focused, it is slightly pushed above the keyboard, but the bottom part of the second input is partially covered by the keyboard and cannot be scrolled.

Screens with react-native-keyboard-aware-scroll-view on iOS, react-native-keyboard-manager on iOS, and react-native-avoid-softinput "component" handle keyboard avoidance similarly - after focusing one of the text fields, form is pushed above the top edge of the keyboard and the user can scroll to the very bottom of modal's content.

On Android, react-native-keyboard-aware-scroll-view screen clips the bottom part of the second input when it is focused.

On Android, screen with android:windowSoftInputMode="adjustResize" does not move up form when the keyboard is displayed. That is because React Native Modal uses Android Dialog control under the hood, which has its own android:windowSoftInputMode property that, unfortunately, cannot be set from JS code.

react-native-avoid-softinput "module" does not work with RN Modal components - when the keyboard pops up, the form stays in its position, and content under the keyboard cannot be accessed.

Bottom sheet modal example

The next example also uses React Native Modal component, this time, it displays its children as a bottom sheet. Similar to the previous example, it accepts a ModalContentWrapper prop (defaults to View component).


const DefaultModalContentWrapper: React.FC = ({
children
}) => <View style={styles.wrapper}> {children} </View>; interface Props { ModalContentWrapper?: React.FC } const ModalBottomSheetExample: React.FC<Props> = ({
ModalContentWrapper = DefaultModalContentWrapper
}) => { const [ isModalVisible, setIsModalVisible ] = useState(false); function closeModal() { setIsModalVisible(false); } function openModal() { setIsModalVisible(true); } return <View style={commonStyles.screenContainer}> <Button onPress={openModal} title="Open modal" /> <RNModal animationType="slide" onRequestClose={closeModal} statusBarTranslucent={true} style={styles.modal} supportedOrientations={[ 'landscape', 'portrait' ]} transparent={true} visible={isModalVisible} > <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={styles.modalContent}> <Pressable onPress={closeModal} style={styles.modalContent}> <ModalContentWrapper> <View style={styles.container}> <Text style={styles.header}>Bottom sheet header</Text> <TextInput style={styles.input} /> <View style={styles.buttonContainer}> <Button onPress={closeModal} title="Confirm" /> </View> </View> </ModalContentWrapper> </Pressable> </SafeAreaView> </RNModal> </View>; };

As in regular bottom sheet case, react-native-keyboard-aware-scroll-view will not be checked, because there is no scroll component used inside example.

To consider the solution successful, it should handle:

  • pushing whole bottom sheet content above the top edge of the keyboard

KeyboardAvoidingView + android:windowSoftInputMode=”adjustPan”


const BottomSheetModalContentWrapper: React.FC = ({ children }) => {
  return <KeyboardAvoidingView
    behavior="position"
    contentContainerStyle={styles.keyboardAvoidingView}
    style={styles.keyboardAvoidingView}>
    {children}
  </KeyboardAvoidingView>;
};

const KeyboardAvoidingViewModalBottomSheetScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustPan();
    return () => {
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);
  
  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={commonStyles.screenContainer}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll} > <ModalBottomSheetExample
ModalContentWrapper={BottomSheetModalContentWrapper}
/> </ScrollView> </SafeAreaView>; };

react-native-keyboard-manager + android:windowSoftInputMode=”adjustResize”


const IQKeyboardManagerModalBottomSheetScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    if (Platform.OS !== 'ios') {
      AvoidSoftInput.setAdjustResize();
    } else {
      RNKeyboardManager.setEnable(true);
      RNKeyboardManager.setKeyboardDistanceFromTextField(100);
    }

    return () => {
      if (Platform.OS !== 'ios') {
        AvoidSoftInput.setDefaultAppSoftInputMode();
      } else {
        RNKeyboardManager.setEnable(false);
// Default value RNKeyboardManager.setKeyboardDistanceFromTextField(10); } }; }, []); useFocusEffect(onFocusEffect); return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <ModalBottomSheetExample /> </ScrollView> </SafeAreaView>; };

react-native-avoid-softinput - native module


const AvoidSoftInputModuleModalBottomSheetScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustNothing();
    AvoidSoftInput.setEnabled(true);
    AvoidSoftInput.setAvoidOffset(100);
    return () => {
      AvoidSoftInput.setEnabled(false);
      AvoidSoftInput.setDefaultAppSoftInputMode();
      AvoidSoftInput.setAvoidOffset(0); // Default value
    };
  }, []);

  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <ModalBottomSheetExample /> </ScrollView> </SafeAreaView>; };

react-native-avoid-softinput - native component


const BottomSheetModalContentWrapper: React.FC = ({ children }) => {
  return <AvoidSoftInputView style={styles.avoidSoftInputView}>
    {children}
  </AvoidSoftInputView>;
};

const AvoidSoftInputViewModalBottomSheetScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustNothing();
    return () => {
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);

  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={commonStyles.screenContainer}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <ModalBottomSheetExample
ModalContentWrapper={BottomSheetModalContentWrapper}
/> </ScrollView> </SafeAreaView>; };

Results:

In screens with KeyboardAvoidingView, react-native-keyboard-manager on iOS, and react-native-avoid-softinput "component", when the keyboard is shown, the whole bottom sheet is pushed above the keyboard and its content is easily accessible.

As in the previous example, Android screen with android:windowSoftInputMode="adjustResize" attribute and react-native-avoid-softinput "module" screen will not handle the displayed keyboard.

Portal form example

The next example will use Portal component from @gorhom/portal library. It accepts PortalContentWrapper prop, which like in previous examples will wrap displayed form content (default one uses ScrollView).


const DefaultPortalContentWrapper: React.FC = ({
children
}) => <View style={styles.scrollWrapper}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll} > {children} </ScrollView> </View>; interface Props { PortalContentWrapper?: React.FC } /** * Remember to render it under `PortalProvider` from `@gorhom/portal` or
* `BottomSheetModalProvider` from `@gorhom/bottom-sheet` */ const PortalFormExample: React.FC<Props> = ({
PortalContentWrapper = DefaultPortalContentWrapper
}) => { //... return <View style={commonStyles.screenContainer}> <Button onPress={openPortal} title="Open portal" /> {isPortalVisible ? <Portal> <View style={styles.portal}> <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={styles.portalContent}> <View style={styles.container}> <View style={styles.wrapper}> <CloseButton onPress={closePortal} /> </View> <PortalContentWrapper> <View style={styles.spacer} /> <View style={styles.inputsContainer}> <TextInput placeholder="Single line input" style={styles.input} /> <TextInput multiline placeholder="Multiline input" style={[ styles.input, styles.multilineInput ]} /> </View> </PortalContentWrapper> </View> </SafeAreaView> </View> </Portal> : null} </View>; };

To consider the solution successful, it should handle:

  • applying bottom padding or translation
  • pushing input above the keyboard

KeyboardAvoidingView + android:windowSoftInputMode=”adjustPan”


const KeyboardAvoidingViewPortalContentWrapper: React.FC = ({
children
}) => <KeyboardAvoidingView behavior="position" contentContainerStyle={styles.keyboardAvoidingView} style={styles.keyboardAvoidingView}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll} > {children} </ScrollView> </KeyboardAvoidingView>; const KeyboardAvoidingViewPortalScreen: React.FC = () => { const onFocusEffect = useCallback(() => { AvoidSoftInput.setAdjustPan(); return () => { AvoidSoftInput.setDefaultAppSoftInputMode(); }; }, []); useFocusEffect(onFocusEffect); return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <PortalFormExample PortalContentWrapper={KeyboardAvoidingViewPortalContentWrapper} /> </SafeAreaView>; };

react-native-keyboard-aware-scroll-view + android:windowSoftInputMode=”adjustPan”


const KeyboardAwareScrollViewPortalContentWrapper: React.FC = ({
children
}) => <KeyboardAwareScrollView enableOnAndroid={true} enableResetScrollToCoords={false} bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> {children} </KeyboardAwareScrollView>; const KeyboardAwareScrollViewPortalScreen: React.FC = () => { const onFocusEffect = useCallback(() => { AvoidSoftInput.setAdjustPan(); return () => { AvoidSoftInput.setDefaultAppSoftInputMode(); }; }, []); useFocusEffect(onFocusEffect); return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <PortalFormExample PortalContentWrapper={KeyboardAwareScrollViewPortalContentWrapper} /> </SafeAreaView>; };

react-native-keyboard-manager + android:windowSoftInputMode=”adjustResize”


const IQKeyboardManagerPortalScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    if (Platform.OS !== 'ios') {
      AvoidSoftInput.setAdjustResize();
    } else {
      RNKeyboardManager.setEnable(true);
    }

    return () => {
      if (Platform.OS !== 'ios') {
        AvoidSoftInput.setDefaultAppSoftInputMode();
      } else {
        RNKeyboardManager.setEnable(false);
      }
    };
  }, []);
  
  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <PortalFormExample /> </SafeAreaView>; };

react-native-avoid-softinput - native module


const AvoidSoftInputModulePortalScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustNothing();
    return () => {
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);
  
  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <PortalFormExample /> </SafeAreaView>; };

react-native-avoid-softinput - native component


const AvoidSoftInputViewPortalContentWrapper: React.FC = ({
children
}) => <AvoidSoftInputView style={styles.avoidSoftInput}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll} > {children} </ScrollView> </AvoidSoftInputView>; const AvoidSoftInputViewPortalScreen: React.FC = () => { const onFocusEffect = useCallback(() => { AvoidSoftInput.setAdjustNothing(); return () => { AvoidSoftInput.setDefaultAppSoftInputMode(); }; }, []); useFocusEffect(onFocusEffect); return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <PortalFormExample PortalContentWrapper={AvoidSoftInputViewPortalContentWrapper} /> </SafeAreaView>; };

Results:

Screens with KeyboardAvoidingView, react-native-keyboard-aware-scroll-view, react-native-keyboard-manager on iOS, and react-native-avoid-softinput "component" will behave the same as in modal form example

As opposed to modal form example, the screen with android:windowSoftInputMode="adjustResize" on Android will match its behavior with iOS.

react-native-avoid-softinput "module" screen has the same behavior as in modal form example.

Multiple inputs example

Let's end with a very unusual example - full-screen list of text fields.


const MultipleInputsFormExample: React.FC = () => {
  return <>
    <TextInput placeholder="1" style={styles.input} />
    <TextInput placeholder="2" style={styles.input} />
    <TextInput placeholder="3" style={styles.input} />
    <TextInput placeholder="4" style={styles.input} />
    <TextInput placeholder="5" style={styles.input} />
    <TextInput placeholder="6" style={styles.input} />
    <TextInput placeholder="7" style={styles.input} />
    <TextInput placeholder="8" style={styles.input} />
    <TextInput placeholder="9" style={styles.input} />
    <TextInput placeholder="10" style={styles.input} />
    <TextInput placeholder="11" style={styles.input} />
    <TextInput placeholder="12" style={styles.input} />
    <TextInput placeholder="13" style={styles.input} />
  </>;
};

Form component will be wrapped in a full-screen scroll component (defaults to ScrollView).

To consider the solution successful, it should handle:

  • applying bottom padding or translation
  • pushing focused input above the keyboard, but below visible screen’s top edge

KeyboardAvoidingView + android:windowSoftInputMode=”adjustPan”


const KeyboardAvoidingViewMultipleInputsFormScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustPan();
    return () => {
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);
  
  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'left', 'right' ]}
style={commonStyles.screenContainer}> <KeyboardAvoidingView behavior="position" style={styles.keyboardAvoidingView}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll} > <MultipleInputsFormExample /> </ScrollView> </KeyboardAvoidingView> </SafeAreaView>; };

react-native-keyboard-aware-scroll-view + android:windowSoftInputMode=”adjustPan”


const KeyboardAwareScrollViewMultipleInputsFormScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustPan();
    return () => {
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);
  
  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={commonStyles.screenContainer}> <KeyboardAwareScrollView enableOnAndroid={true} enableResetScrollToCoords={false} bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <MultipleInputsFormExample /> </KeyboardAwareScrollView> </SafeAreaView>; };

react-native-keyboard-manager + android:windowSoftInputMode=”adjustResize”


const IQKeyboardManagerMultipleInputsFormScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    if (Platform.OS !== 'ios') {
      AvoidSoftInput.setAdjustResize();
    } else {
      RNKeyboardManager.setEnable(true);
    }

    return () => {
      if (Platform.OS !== 'ios') {
        AvoidSoftInput.setDefaultAppSoftInputMode();
      } else {
        RNKeyboardManager.setEnable(false);
      }
    };
  }, []);

  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={commonStyles.screenContainer}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <MultipleInputsFormExample /> </ScrollView> </SafeAreaView>; };

react-native-avoid-softinput - native module


const AvoidSoftInputModuleMultipleInputsFormScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustNothing();
    AvoidSoftInput.setEnabled(true);
    return () => {
      AvoidSoftInput.setEnabled(false);
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);

  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={commonStyles.screenContainer}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <MultipleInputsFormExample /> </ScrollView> </SafeAreaView>; };

react-native-avoid-softinput - native component


const AvoidSoftInputViewMultipleInputsFormScreen: React.FC = () => {
  const onFocusEffect = useCallback(() => {
    AvoidSoftInput.setAdjustNothing();
    return () => {
      AvoidSoftInput.setDefaultAppSoftInputMode();
    };
  }, []);

  useFocusEffect(onFocusEffect);

  return <SafeAreaView
edges={[ 'bottom', 'left', 'right' ]}
style={commonStyles.screenContainer}> <AvoidSoftInputView style={styles.avoidSoftInputView}> <ScrollView bounces={false} contentContainerStyle={commonStyles.scrollContainer} contentInsetAdjustmentBehavior="always" overScrollMode="always" showsVerticalScrollIndicator={true} style={commonStyles.scroll}> <MultipleInputsFormExample /> </ScrollView> </AvoidSoftInputView> </SafeAreaView>; };

Results:

Screen with KeyboardAvoidingView has a little bit different behavior depending on the platform. On iOS, the last text field is completely covered even if a user tries to scroll to the very bottom of the screen. On Android amount of applied padding seems to be random. On both platforms input's position is changed, even if it won't be covered by a soft keyboard.

The rest of screens handle displayed keyboard in similar fashion - focused inputs are scrolled above the keyboard only, when it is necessary. The whole ScrollView content is accessible when a keyboard is visible.

Summary

It's hard to achieve consistent behavior across both platforms and different app use cases. Usually, it's more complex than "Let's wrap it in KeyboardAvoidingView, it's built-in, so it should work, shouldn't it?". Layouts can have different types, sometimes, a layout can have different parts that need to be handled separately (e.g. form inside scrollable component together with CTA button positioned at the bottom of the screen and outside scrollable component).

KeyboardAvoidingView with androidWindowSoftInputMode="adjustPan" can work with very basic forms and easy modal-based bottom sheets, but it will struggle with more complicated screens. react-native-keyboard-aware-scroll-view with androidWindowSoftInputMode="adjustPan" works very well in screens that have scrollable content and its behavior is similar on Android and iOS. react-native-keyboard-manager is a "plug'n'play" solution on iOS that handled all test cases, androidWindowSoftInputMode="adjustResize" covers most of scenarios on Android. react-native-avoid-softinput gives consistent behavior on both platforms and handles all test cases either with its "module" or "component". All those solutions are worth to be considered depending on your use case. In one project, a "built-in" solution will work fine, in others it will require introducing more than one solution, including some custom ones, to achieve the desired effect on all layouts.

Photo of Mateusz Mędrek

More posts by this author

Mateusz Mędrek

Codestories Newsletter  Check what has recently been hot in European Tech.   Subscribe now

We're Netguru!


At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency
Let's talk business!

Trusted by:

  • Vector-5
  • Babbel logo
  • Merc logo
  • Ikea logo
  • Volkswagen logo
  • UBS_Home