Skip to content

Instantly share code, notes, and snippets.

@SyedTayyabUlMazhar
Created September 12, 2025 18:55
Show Gist options
  • Save SyedTayyabUlMazhar/00de8fe02b620c8bf87205d167bbb096 to your computer and use it in GitHub Desktop.
Save SyedTayyabUlMazhar/00de8fe02b620c8bf87205d167bbb096 to your computer and use it in GitHub Desktop.
A modal that extends react-native-modal. Includes presenting modes(sheet, modal). Uses Portal as well.
import React, { useEffect, useId, useState } from "react";
import {
BackHandler,
Dimensions,
LayoutChangeEvent,
Platform,
TouchableOpacity,
View,
} from "react-native";
import ReactNativeModal from "react-native-modal";
import styles from "./style";
import { BaseModalProps } from "./types";
import { FontAwesomeIcon } from "@fortawesome/react-native-fontawesome";
import { COLORS } from "@/constants/Colors";
import { Portal } from "@rn-primitives/portal";
import { useBoolean } from "@/hooks";
const Config = {
modal: {
animationInTiming: 50,
animationOutTiming: 200,
animationIn: "fadeIn",
animationOut: "fadeOut",
swipeDirection: undefined,
style: undefined,
containerStyle: styles.modalContainer,
...(Platform.OS === "android"
? { deviceHeight: Dimensions.get("screen").height }
: {}),
},
sheet: {
animationInTiming: 400,
animationOutTiming: 700,
animationIn: "slideInUp",
animationOut: "slideOutDown",
swipeDirection: "down",
style: styles.sheet,
containerStyle: styles.sheetContainer,
},
} as const;
export const BaseModal = (props: BaseModalProps) => {
const {
modalState,
mode,
children,
height,
minHeight,
maxHeight,
name,
portal,
closeButtonStyle,
} = props;
const id = useId();
const isSheet = mode === "sheet";
const { containerStyle, style, ...config } = Config[mode];
const shouldShowModalContent = useBoolean();
const [contentHeight, setContentHeight] = useState(0);
const shouldCalculateContentHeight = isSheet && !contentHeight;
const close = () => {
modalState.setFalse();
};
const onLayout = (e: LayoutChangeEvent) => {
setContentHeight(e.nativeEvent.layout.height);
};
useEffect(() => {
const backHandler = BackHandler.addEventListener(
"hardwareBackPress",
() => {
close();
return true;
}
);
return () => backHandler.remove();
}, []);
const renderModal = () => {
return (
<ReactNativeModal
backdropOpacity={0.4}
onBackdropPress={close}
isVisible={modalState.value}
backdropTransitionOutTiming={1}
coverScreen={name ? false : true}
hideModalContentWhileAnimating
onModalWillShow={shouldShowModalContent.setTrue}
onModalWillHide={shouldShowModalContent.setFalse}
backdropTransitionInTiming={1}
statusBarTranslucent
{...config}
{...(isSheet
? {
swipeThreshold: contentHeight * 0.4,
onSwipeComplete: close,
propagateSwipe: true,
scrollOffset: 1,
scrollTo: () => {},
}
: {})}
style={[style, props.style]}
>
{shouldShowModalContent.value ? (
<View
style={[
containerStyle,
props.contentContainerStyle,
{ height, minHeight, maxHeight },
]}
onLayout={shouldCalculateContentHeight ? onLayout : undefined}
>
{!isSheet ? (
<TouchableOpacity
style={[styles.closeButton, closeButtonStyle]}
onPress={close}
>
<FontAwesomeIcon
icon={["far", "xmark"]}
size={20}
color={COLORS.BLACK}
/>
</TouchableOpacity>
) : null}
{isSheet ? <View style={styles.handleIndicator} /> : null}
{children}
</View>
) : null}
</ReactNativeModal>
);
};
if (name || portal) {
return <Portal name={"portal-" + id}>{renderModal()}</Portal>;
}
return renderModal();
};
import { COLORS } from "@/constants/Colors";
import { scale, verticalScale } from "@/utilities/Metrics";
import { StyleSheet } from "react-native";
const styles = StyleSheet.create({
closeButton: {
position: "absolute",
top: verticalScale(16),
right: scale(16),
zIndex: 1
},
sheet: {
margin: 0,
justifyContent: "flex-end",
},
handleIndicator: {
backgroundColor: COLORS.SHEET_HANDLE,
width: scale(42),
height: scale(6),
borderRadius: 8,
opacity: 0.2,
marginTop: verticalScale(24),
marginBottom: verticalScale(24),
alignSelf: "center",
},
modalContainer: {
backgroundColor: COLORS.WHITE,
borderRadius: scale(15),
paddingHorizontal: scale(24),
paddingVertical: verticalScale(48),
},
sheetContainer: {
backgroundColor: COLORS.WHITE,
borderTopLeftRadius: scale(15),
borderTopRightRadius: scale(15),
paddingHorizontal: scale(24),
paddingBottom: verticalScale(48),
},
});
export default styles;
import { UseBooleanReturn } from "@/hooks/useBoolean";
import React from "react";
import { StyleProp, ViewStyle } from "react-native";
export type BaseModalProps = React.PropsWithChildren<{
modalState: UseBooleanReturn;
mode: "sheet" | "modal";
contentContainerStyle?: StyleProp<ViewStyle>;
height?: ViewStyle["height"];
minHeight?: ViewStyle["minHeight"];
maxHeight?: ViewStyle["maxHeight"];
/**
* @deprecated use {@link BaseModalProps.portal} instead.
*
* For use with Portal, if provided, the modal will be rendered inside a Portal
* and cover screen will be set to false
*/
name?: string;
portal?: boolean;
closeButtonStyle?: StyleProp<ViewStyle>;
/**
* Style prop passed to the react-native-modal
*/
style?: StyleProp<ViewStyle>;
}>;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment