Avoid Keyboard in React Native Like a Pro
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:
- KeyboardAvoidingView +
android:windowSoftInputMode=”adjustPan”
- react-native-keyboard-aware-scroll-view +
android:windowSoftInputMode=”adjustPan”
- react-native-keyboard-manager +
android:windowSoftInputMode=”adjustResize”
- react-native-avoid-softinput - native module & native component
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.