Skip to content

Modal

Modal provides a comprehensive solution for overlays and dialogs with built-in animations, multiple positioning options, and flexible content management. It supports various animation types, backdrop interactions, and includes dedicated components for header, content, and footer sections.

import { Modal, ModalHeader, ModalContent, ModalFooter } from 'rnc-theme';
<Modal visible={isVisible} onClose={() => setIsVisible(false)}>
<ModalHeader title="Confirmation" />
<ModalContent>
<Text>Are you sure you want to continue?</Text>
</ModalContent>
<ModalFooter>
<Button onPress={() => setIsVisible(false)}>
<ButtonText>Cancel</ButtonText>
</Button>
<Button variant="primary" onPress={handleConfirm}>
<ButtonText>Confirm</ButtonText>
</Button>
</ModalFooter>
</Modal>
PropTypeDefaultDescription
visibleboolean-Controls modal visibility
onClose() => void-Callback when modal should close
childrenReact.ReactNode-Modal content
sizeComponentSize'md'Modal size (xs, sm, md, lg, xl)
variantComponentVariant'default'Visual style variant
position'center' | 'top' | 'bottom''center'Modal position on screen
animation'slide' | 'fade' | 'scale''fade'Animation type
closeOnBackdropbooleantrueClose modal when backdrop is pressed
showCloseButtonbooleantrueShow close button in top-right
backdropOpacitynumber0.5Backdrop opacity (0-1)
animationDurationnumber250Animation duration in milliseconds
paddingkeyof Theme['spacing']'lg'Internal padding
marginkeyof Theme['spacing']-External margin
borderRadiuskeyof Theme['components']['borderRadius']'lg'Border radius
elevationnumber8Android elevation
shadowOpacitynumber0.15iOS shadow opacity
backgroundColorstring-Custom background color
PropTypeDefaultDescription
titlestring-Header title text
subtitlestring-Header subtitle text
childrenReact.ReactNode-Custom header content
showCloseButtonbooleanfalseShow close button in header
onClose() => void-Close button callback
paddingkeyof Theme['spacing']'md'Header padding
titleVariantkeyof Theme['typography']'subtitle'Title typography variant
subtitleVariantkeyof Theme['typography']'body'Subtitle typography variant
borderBottombooleanfalseShow bottom border
PropTypeDefaultDescription
childrenReact.ReactNode-Content to display
scrollablebooleanfalseMake content scrollable
paddingkeyof Theme['spacing']'md'Content padding
styleStyleProp<ViewStyle>-Additional styles
PropTypeDefaultDescription
childrenReact.ReactNode-Footer content (usually buttons)
paddingkeyof Theme['spacing']'md'Footer padding
showBorderbooleanfalseShow top border
justifyContentFlexAlignType'flex-end'Button alignment
styleViewStyle-Additional footer styles
PositionDescriptionAnimation Behavior
centerCenter of screenFade/scale from center
topTop of screenSlide down from top
bottomBottom of screenSlide up from bottom
AnimationDescriptionBest Use Case
fadeOpacity transitionGeneral purpose, subtle
scaleScale from small to fullAttention-grabbing alerts
slideSlide in from positionBottom sheets, drawers
const ConfirmationModal = ({ visible, onClose, onConfirm, title, message }) => {
return (
<Modal
visible={visible}
onClose={onClose}
size="sm"
animation="scale"
variant="primary"
>
<ModalHeader title={title} />
<ModalContent>
<Text style={{ textAlign: 'center', marginVertical: 16 }}>
{message}
</Text>
</ModalContent>
<ModalFooter justifyContent="space-between">
<Button variant="outline" onPress={onClose} style={{ flex: 1, marginRight: 8 }}>
<ButtonText>Cancel</ButtonText>
</Button>
<Button variant="primary" onPress={onConfirm} style={{ flex: 1, marginLeft: 8 }}>
<ButtonText>Confirm</ButtonText>
</Button>
</ModalFooter>
</Modal>
);
};
// Usage
<ConfirmationModal
visible={showDeleteModal}
onClose={() => setShowDeleteModal(false)}
onConfirm={handleDelete}
title="Delete Item"
message="Are you sure you want to delete this item? This action cannot be undone."
/>
const ProfileEditModal = ({ visible, onClose, user, onSave }) => {
const [name, setName] = useState(user?.name || '');
const [email, setEmail] = useState(user?.email || '');
const [loading, setLoading] = useState(false);
const handleSave = async () => {
setLoading(true);
try {
await onSave({ name, email });
onClose();
} catch (error) {
// Handle error
} finally {
setLoading(false);
}
};
return (
<Modal
visible={visible}
onClose={onClose}
size="lg"
position="center"
>
<ModalHeader
title="Edit Profile"
subtitle="Update your personal information"
borderBottom
/>
<ModalContent scrollable>
<VStack spacing="lg" padding="md">
<VStack spacing="sm">
<Text>Full Name</Text>
<TextInput
value={name}
onChangeText={setName}
placeholder="Enter your name"
/>
</VStack>
<VStack spacing="sm">
<Text>Email Address</Text>
<TextInput
value={email}
onChangeText={setEmail}
placeholder="Enter your email"
keyboardType="email-address"
/>
</VStack>
</VStack>
</ModalContent>
<ModalFooter showBorder justifyContent="flex-end">
<Button variant="outline" onPress={onClose} disabled={loading}>
<ButtonText>Cancel</ButtonText>
</Button>
<Button
variant="primary"
onPress={handleSave}
loading={loading}
style={{ marginLeft: 12 }}
>
<ButtonText>Save Changes</ButtonText>
</Button>
</ModalFooter>
</Modal>
);
};
const ImageGalleryModal = ({ visible, onClose, images, initialIndex = 0 }) => {
const [currentIndex, setCurrentIndex] = useState(initialIndex);
const goToPrevious = () => {
setCurrentIndex(prev => prev > 0 ? prev - 1 : images.length - 1);
};
const goToNext = () => {
setCurrentIndex(prev => prev < images.length - 1 ? prev + 1 : 0);
};
return (
<Modal
visible={visible}
onClose={onClose}
size="xl"
position="center"
animation="fade"
backgroundColor="rgba(0, 0, 0, 0.95)"
showCloseButton={false}
>
<ModalHeader
showCloseButton
onClose={onClose}
title={`${currentIndex + 1} of ${images.length}`}
style={{ backgroundColor: 'transparent' }}
/>
<ModalContent style={{ flex: 1, justifyContent: 'center' }}>
<Image
source={{ uri: images[currentIndex]?.url }}
style={{
width: '100%',
height: 300,
resizeMode: 'contain'
}}
/>
</ModalContent>
<ModalFooter justifyContent="space-between">
<Button
variant="ghost"
onPress={goToPrevious}
disabled={images.length <= 1}
>
<ButtonIcon icon={<ChevronLeftIcon />} position="left" />
<ButtonText>Previous</ButtonText>
</Button>
<HStack spacing="xs">
{images.map((_, index) => (
<TouchableOpacity
key={index}
onPress={() => setCurrentIndex(index)}
style={{
width: 8,
height: 8,
borderRadius: 4,
backgroundColor: index === currentIndex ? '#fff' : 'rgba(255,255,255,0.3)'
}}
/>
))}
</HStack>
<Button
variant="ghost"
onPress={goToNext}
disabled={images.length <= 1}
>
<ButtonText>Next</ButtonText>
<ButtonIcon icon={<ChevronRightIcon />} position="right" />
</Button>
</ModalFooter>
</Modal>
);
};
const ActionSheetModal = ({ visible, onClose, options }) => {
return (
<Modal
visible={visible}
onClose={onClose}
position="bottom"
animation="slide"
size="md"
borderRadius="xl"
>
<ModalHeader title="Choose Action" />
<ModalContent>
<VStack spacing="sm">
{options.map((option, index) => (
<TouchableOpacity
key={index}
onPress={() => {
option.onPress();
onClose();
}}
style={{
padding: 16,
borderRadius: 8,
backgroundColor: option.destructive ?
'rgba(255, 59, 48, 0.1)' :
'rgba(0, 0, 0, 0.05)'
}}
>
<HStack spacing="md" align="center">
{option.icon && (
<View style={{ width: 24, height: 24 }}>
{option.icon}
</View>
)}
<Text style={{
flex: 1,
color: option.destructive ? '#FF3B30' : undefined
}}>
{option.title}
</Text>
</HStack>
</TouchableOpacity>
))}
</VStack>
</ModalContent>
</Modal>
);
};
// Usage
const menuOptions = [
{
title: 'Edit',
icon: <EditIcon />,
onPress: handleEdit
},
{
title: 'Share',
icon: <ShareIcon />,
onPress: handleShare
},
{
title: 'Delete',
icon: <DeleteIcon />,
onPress: handleDelete,
destructive: true
}
];
<ActionSheetModal
visible={showActionSheet}
onClose={() => setShowActionSheet(false)}
options={menuOptions}
/>
const LoadingModal = ({ visible, message = 'Loading...', progress }) => {
return (
<Modal
visible={visible}
onClose={() => {}} // Prevent closing during loading
size="sm"
animation="fade"
closeOnBackdrop={false}
showCloseButton={false}
>
<ModalContent style={{ alignItems: 'center', padding: 32 }}>
<ActivityIndicator size="large" style={{ marginBottom: 16 }} />
<Text style={{ textAlign: 'center', marginBottom: 8 }}>
{message}
</Text>
{progress !== undefined && (
<View style={{
width: '100%',
height: 4,
backgroundColor: '#e0e0e0',
borderRadius: 2,
overflow: 'hidden'
}}>
<View style={{
width: `${progress * 100}%`,
height: '100%',
backgroundColor: '#007AFF'
}} />
</View>
)}
</ModalContent>
</Modal>
);
};
const NotificationModal = ({
visible,
onClose,
type = 'info',
title,
message,
actions
}) => {
const getTypeStyles = () => {
switch (type) {
case 'success':
return { variant: 'success', icon: <CheckCircleIcon /> };
case 'error':
return { variant: 'error', icon: <XCircleIcon /> };
case 'warning':
return { variant: 'warning', icon: <AlertTriangleIcon /> };
default:
return { variant: 'info', icon: <InfoIcon /> };
}
};
const typeStyles = getTypeStyles();
return (
<Modal
visible={visible}
onClose={onClose}
size="sm"
variant={typeStyles.variant}
position="top"
animation="slide"
>
<ModalContent>
<HStack spacing="md" align="flex-start">
<View style={{ marginTop: 4 }}>
{typeStyles.icon}
</View>
<VStack spacing="xs" style={{ flex: 1 }}>
<Text style={{ fontWeight: '600', fontSize: 16 }}>
{title}
</Text>
<Text style={{ color: '#666', lineHeight: 20 }}>
{message}
</Text>
</VStack>
</HStack>
</ModalContent>
{actions && (
<ModalFooter>
{actions.map((action, index) => (
<Button
key={index}
variant={action.primary ? 'primary' : 'outline'}
onPress={() => {
action.onPress();
onClose();
}}
style={{
marginLeft: index > 0 ? 8 : 0,
flex: actions.length > 1 ? 1 : undefined
}}
>
<ButtonText>{action.title}</ButtonText>
</Button>
))}
</ModalFooter>
)}
</Modal>
);
};
const useModal = (initialVisible = false) => {
const [visible, setVisible] = useState(initialVisible);
const show = useCallback(() => setVisible(true), []);
const hide = useCallback(() => setVisible(false), []);
const toggle = useCallback(() => setVisible(prev => !prev), []);
return {
visible,
show,
hide,
toggle,
setVisible
};
};
// Usage
const MyComponent = () => {
const editModal = useModal();
const deleteModal = useModal();
return (
<>
<Button onPress={editModal.show}>
<ButtonText>Edit</ButtonText>
</Button>
<Modal visible={editModal.visible} onClose={editModal.hide}>
{/* Edit modal content */}
</Modal>
<Modal visible={deleteModal.visible} onClose={deleteModal.hide}>
{/* Delete modal content */}
</Modal>
</>
);
};
const ModalStack = () => {
const [modals, setModals] = useState([]);
const pushModal = (modalComponent) => {
setModals(prev => [...prev, { id: Date.now(), component: modalComponent }]);
};
const popModal = () => {
setModals(prev => prev.slice(0, -1));
};
return (
<>
{modals.map((modal, index) => (
<View key={modal.id} style={{ zIndex: 1000 + index }}>
{React.cloneElement(modal.component, {
onClose: popModal
})}
</View>
))}
</>
);
};
<Modal
animation="fade"
animationDuration={300}
/>

User Experience

  • Always provide a clear way to close the modal (backdrop tap or close button)
  • Use appropriate animation types: fade for general use, scale for important alerts, slide for sheets
  • Keep modal content focused and avoid nested scrolling when possible

Performance

  • Use conditional rendering to avoid mounting modals when not needed
  • Implement proper cleanup in onClose handlers
  • Consider using React.memo for complex modal content