skip to content

In React Native, how do you show a dimmed full-screen loading overlay with Modal and ActivityIndicator while a photo backup runs?

level: juniorimportance: should knowfreq 42%

answer

  1. visible from state, default true
  2. transparent is not a scrim
  3. fade, not the default none
  4. spinner size: numbers on Android only
  5. stopped spinner keeps its box

basics

~20 s

Render 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 s

Drive 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 lines
tsx
import {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

for a junior

Recall the recipe: visible from state, transparent, animationType fade, a translucent full-size View, and a large ActivityIndicator in the centre.

for a middle

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.

for a senior

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.

for a principal

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.