In React Native, how do you show a dimmed full-screen loading overlay with Modal and ActivityIndicator while a photo backup runs?
answer
- visible from state, default true
- transparent is not a scrim
- fade, not the default none
- spinner size: numbers on Android only
- stopped spinner keeps its box
basics
~20 sRender a Modal with visible bound to backup state, transparent and animationType fade, fill it with a translucent full-size View, and centre an ActivityIndicator size large. transparent only removes the background, so the dim is your own View.
solid answer
~40 sDrive the overlay from state with `visible={isBackingUp}`, because `visible` defaults to `true`. Set `transparent` so the screen shows through (on iOS this switches `presentationStyle` to `overFullScreen`) and `animationType="fade"`, since the default is `'none'`. `transparent` does not dim anything: add a `flex: 1` `View` with an `rgba` background and centre an `ActivityIndicator size="large"` in it. `ActivityIndicator` animates by default; `size` accepts `'small'` or `'large'`, and a number only on Android. Its colour defaults to gray on iOS and the system accent on Android. Decide the back-button policy in `onRequestClose`, and clear `isBackingUp` on both success and failure so the overlay cannot get stuck.
code
tsx · 37 linesimport {ActivityIndicator, Modal, StyleSheet, Text, View} from 'react-native';
type BackupOverlayProps = {
isBackingUp: boolean;
uploaded: number;
total: number;
};
export function BackupOverlay({isBackingUp, uploaded, total}: BackupOverlayProps) {
return (
<Modal
visible={isBackingUp}
transparent
animationType="fade"
onRequestClose={() => {
// The upload must finish: ignore the Android back button.
}}>
<View style={styles.scrim}>
<ActivityIndicator size="large" color="#ffffff" />
<Text style={styles.label}>
Backing up {uploaded} of {total}
</Text>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
scrim: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
backgroundColor: 'rgba(0,0,0,0.5)',
},
label: {color: '#ffffff', fontSize: 16},
});go deeper
Recall the recipe: visible from state, transparent, animationType fade, a translucent full-size View, and a large ActivityIndicator in the centre.
Know the defaults that surprise people: visible true, animationType none, the iOS presentation style switching to overFullScreen, a numeric spinner size only on Android, and a stopped spinner keeping its box.
Make the overlay impossible to strand: clear it in success and error paths, choose a deliberate back-button policy, and show progress so a blocking overlay does not look like a hang.
Question whether a blocking overlay is right at all; background uploads with inline progress often serve users better than a modal that freezes the app during long backups.
## The job A photo-backup app uploads hundreds of files. While a manual "Back up now" runs, the product wants a **dimmed, full-screen overlay with a spinner** that stops the user from starting a second backup or deleting photos mid-upload. In React Native that is two core components from `react-native`: **`Modal`** for the overlay and **`ActivityIndicator`** for the spinner. ## Modal props that shape an overlay | Prop | Default | What it does | |---|---|---| | `visible` | `true` | shows or hides the modal; always pass it explicitly | | `animationType` | `'none'` | `'slide'` from the bottom, `'fade'`, or no animation | | `transparent` | `false` | when `true`, the modal's container is transparent so the screen behind shows through | | `backdropColor` | white | container colour when not transparent; ignored when `transparent` is `true` | | `presentationStyle` (iOS) | `fullScreen`, or `overFullScreen` when `transparent` | how iOS presents it; `pageSheet` and `formSheet` matter on larger devices | | `onRequestClose` | none | Android back button and iOS sheet swipes; your dismissal policy | Points that trip people up: - **`visible` defaults to `true`.** A `Modal` rendered without `visible` is on screen immediately. Drive it from state, such as `visible={isBackingUp}`. - **`transparent` does not dim anything.** It only makes the container see-through. The dim comes from your own full-size `View` with a translucent `backgroundColor`, such as `rgba(0,0,0,0.5)`. - **Transparency needs `overFullScreen` on iOS.** It is the default when `transparent` is set. Combining `transparent` with another explicit `presentationStyle` produces a dev warning that the combination is not supported. - **`animationType="fade"`** suits an overlay; `'slide'` suits a sheet-like panel. - Since React Native 0.86, the Modal's `style` prop is forwarded to its inner container `View` without overriding `transparent` or `backdropColor`. ## ActivityIndicator props `ActivityIndicator` renders the platform's native circular spinner: - **`animating`** defaults to `true`. - **`size`** is `'small'` (the default, about 20 high) or `'large'` (about 36). A **number** is supported only on Android. - **`color`** defaults to gray (`#999999`) on iOS and to the system accent colour on Android. - **`hidesWhenStopped`** is iOS-only and defaults to `true`. With `animating={false}` the spinner disappears on both platforms by default: iOS hides it because `hidesWhenStopped` is `true`, and Android makes the native progress view invisible. In both cases the component's box **still takes layout space**. To remove it from layout, render it conditionally. ## Why a Modal and not an absolutely positioned View An absolutely positioned `View` covers only its parent's area and only blocks touches inside it, so native chrome outside that area stays interactive. A `Modal` is presented above the rest of the app's content, and on Android it also takes the back button, which lets you set a policy for it. ## Putting it together 1. Hold `isBackingUp` in state and pass `visible={isBackingUp}`. 2. Set `transparent` and `animationType="fade"`. 3. Fill the modal with a `flex: 1` `View` that has a translucent background, and centre an `ActivityIndicator size="large"` with a short progress label. 4. Decide the back policy in `onRequestClose`: ignore the press while the upload must finish, or cancel the upload and hide the overlay. 5. Set `isBackingUp` to `false` in both the success and the error path, so the overlay can never outlive the work. ## Keeping the overlay honest A blocking overlay is a promise to the user that something is happening. Keep it truthful: - **Show progress, not just motion.** "Backing up 37 of 212" tells the user the app is alive. A spinner alone after thirty seconds looks like a hang. - **Model the states explicitly**, for example `idle`, `backingUp` and `failed`, and derive `visible` from `backingUp`. The overlay can then never be up while the state says the work has ended. - **Surface failures after the overlay closes**, for instance with an `Alert` or an inline banner, rather than leaving a spinner that silently stops. - **Keep the overlay's content light.** It is on screen while the JavaScript thread is also busy coordinating uploads, so avoid heavy re-renders inside it on every progress tick. ## Pitfalls - Forgetting the error path, which leaves the overlay up forever. - Assuming `transparent` adds a scrim. - Assuming `size={48}` works on iOS. - Rendering a `Modal` without `visible` "to toggle later" and getting an overlay on first render.
- Why does the React Native ActivityIndicator still leave a gap after animating is set to false?`animating={false}` hides the native spinner (iOS via `hidesWhenStopped`, which defaults to `true`; Android by making the view invisible), but the component's container keeps its size. To collapse the space, render the indicator conditionally instead of toggling `animating`.
- What happens if a React Native Modal is given transparent together with presentationStyle 'pageSheet' on iOS?React Native logs a dev warning that the combination is not supported: only `overFullScreen` presents with transparency on iOS. Leave `presentationStyle` unset when you set `transparent`, and it defaults to `overFullScreen`.
saying these in an interview costs you the question
- Setting transparent on a Modal automatically dims the screen behind it.
- A Modal is hidden until you set visible to true.
- Modal fades in by default, so animationType can be left out.
- ActivityIndicator accepts a numeric size on both iOS and Android.
- animating={false} removes the ActivityIndicator from the layout.