# How to Implement a React Native Spinner Component That Overlays Other Elements

> Learn to implement a React Native spinner that overlays other elements. Use a Modal and absolute positioning for a full-screen loading indicator.

- Repository: [Meta/react](https://github.com/facebook/react)
- Tags: how-to-guide
- Published: 2026-02-16

---

**You can create a full-screen overlay spinner in React Native by wrapping an `ActivityIndicator` inside a `Modal` component and positioning it with `StyleSheet.absoluteFillObject` to ensure it renders above all other UI elements.**

The React repository demonstrates several architectural patterns for handling loading states and portal-style rendering that apply directly to mobile implementations. By leveraging concepts found in the core codebase—specifically Suspense fallbacks and portal rendering—you can build a reusable spinner component that blocks user interaction during async operations while maintaining clean separation of concerns.

## Core Concepts from the React Source Code

The implementation strategy draws from three fundamental patterns present in the `facebook/react` repository:

| Concept | Source Location | Application to Native Overlays |
|---------|----------------|--------------------------------|
| **Suspense fallback UI** | [[`fixtures/ssr2/src/Spinner.js`](https://github.com/facebook/react/blob/main/fixtures/ssr2/src/Spinner.js)](https://github.com/facebook/react/blob/main/fixtures/ssr2/src/Spinner.js) | Demonstrates minimal spinner markup and state toggling that translates to React Native's `View` and `ActivityIndicator` components. |
| **Portal rendering** | [[`packages/react-dom/src/shared/ReactDOM.js`](https://github.com/facebook/react/blob/main/packages/react-dom/src/shared/ReactDOM.js)](https://github.com/facebook/react/blob/main/packages/react-dom/src/shared/ReactDOM.js) (lines 56‑71) | The `createPortal` implementation shows how to render content outside the normal component tree; in React Native, the `Modal` component provides equivalent functionality. |
| **Full-screen positioning** | [[`fixtures/ssr2/public/main.css`](https://github.com/facebook/react/blob/main/fixtures/ssr2/public/main.css)](https://github.com/facebook/react/blob/main/fixtures/ssr2/public/main.css) (lines 50‑61) | The `.spinner--active` CSS class uses fixed positioning to cover the viewport; this maps directly to `StyleSheet.absoluteFillObject` in React Native. |

## Building the OverlaySpinner Component

Create a dedicated component file that abstracts the overlay logic. The component uses `Modal` on iOS and Android to create a separate root node above the current view hierarchy, while falling back to an absolute-filled `View` for web compatibility.

```typescript
// src/OverlaySpinner.tsx
import React from 'react';
import {
  Modal,
  View,
  ActivityIndicator,
  StyleSheet,
  Platform,
} from 'react-native';

/**
 * OverlaySpinner
 *
 * Props:
 *   visible   – whether the spinner should be shown
 *   dimColor? – optional background dimming color (defaults to transparent black)
 *   size?     – ActivityIndicator size ('small' | 'large')
 */
export const OverlaySpinner = ({
  visible,
  dimColor = 'rgba(0,0,0,0.3)',
  size = 'large',
}: {
  visible: boolean;
  dimColor?: string;
  size?: 'small' | 'large';
}) => {
  if (!visible) return null;

  const content = (
    <View style={[styles.backdrop, {backgroundColor: dimColor}]}>
      <ActivityIndicator size={size} color="#fff" />
    </View>
  );

  return Platform.OS === 'web' ? (
    <View style={StyleSheet.absoluteFill}>{content}</View>
  ) : (
    <Modal transparent visible={visible} animationType="none">
      {content}
    </Modal>
  );
};

const styles = StyleSheet.create({
  backdrop: {
    ...StyleSheet.absoluteFillObject,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default OverlaySpinner;

```

### Handling Cross-Platform Differences

**Native platforms (iOS/Android):** The `Modal` component with `transparent` and `animationType="none"` props creates a portal-like root node that automatically sits above all other views, even when triggered from deeply nested components. This mirrors the behavior of `ReactDOM.createPortal` as implemented in the source.

**Web (React Native for Web):** Since `Modal` is not available in React Native for Web, the component falls back to `StyleSheet.absoluteFill`, which applies `position: fixed` with `top: 0`, `left: 0`, `right: 0`, and `bottom: 0` to cover the entire viewport.

## Usage Example

Integrate the spinner at the root level of your screen components. Because the overlay uses `Modal`, you can place it anywhere in the JSX tree and it will still render above sibling elements.

```typescript
// src/screens/ProductsScreen.tsx
import React, {useState, useEffect} from 'react';
import {View, Text, Button} from 'react-native';
import OverlaySpinner from '../OverlaySpinner';

export default function ProductsScreen() {
  const [loading, setLoading] = useState(false);
  const [products, setProducts] = useState<Array<string>>([]);

  const fetchProducts = async () => {
    setLoading(true);
    const data = await new Promise<Array<string>>(resolve =>
      setTimeout(() => resolve(['Apple', 'Banana', 'Cherry']), 2000),
    );
    setProducts(data);
    setLoading(false);
  };

  useEffect(() => {
    fetchProducts();
  }, []);

  return (
    <View style={{flex: 1, padding: 16}}>
      <Button title="Reload" onPress={fetchProducts} />
      {products.map(p => (
        <Text key={p}>{p}</Text>
      ))}

      <OverlaySpinner visible={loading} dimColor="rgba(0,0,0,0.5)" />
    </View>
  );
}

```

## Why This Pattern Works

The architecture aligns with React's core principles for managing UI layering and state:

*   **Portal semantics:** The `Modal` component creates a separate native view hierarchy, equivalent to how `ReactDOM.createPortal` (found in [`packages/react-dom/src/shared/ReactDOM.js`](https://github.com/facebook/react/blob/main/packages/react-dom/src/shared/ReactDOM.js)) renders children into a DOM node outside the parent hierarchy.
*   **Absolute positioning:** The `StyleSheet.absoluteFillObject` spread into the backdrop style applies the same full-screen coverage logic as the `.spinner--active` CSS class defined in [`fixtures/ssr2/public/main.css`](https://github.com/facebook/react/blob/main/fixtures/ssr2/public/main.css).
*   **Platform abstraction:** Using `ActivityIndicator` provides the native platform's standard loading animation, consistent with the minimal SVG spinner approach shown in [`fixtures/ssr2/src/Spinner.js`](https://github.com/facebook/react/blob/main/fixtures/ssr2/src/Spinner.js).

## Summary

*   Use **`Modal`** with `transparent` prop to render the spinner above all other elements without disrupting the component tree structure.
*   Apply **`StyleSheet.absoluteFillObject`** to ensure the overlay covers the entire screen regardless of device dimensions.
*   Import **`ActivityIndicator`** from `react-native` to display the platform-native spinning animation.
*   Handle web compatibility by detecting `Platform.OS === 'web'` and substituting a `View` with absolute positioning for the `Modal` component.
*   Place the `OverlaySpinner` component at any level in your JSX; it will always render on top due to the separate root node created by `Modal`.

## Frequently Asked Questions

### Should I use Modal or a regular View with absolute positioning for the overlay?

Use **`Modal`** for iOS and Android applications because it creates a separate window that sits above the current view hierarchy, ensuring the spinner remains on top even if parent containers have `zIndex` or overflow constraints. Only use a regular `View` with absolute positioning for web platforms where `Modal` is unavailable, or for specific cases where you want the overlay contained within a parent view rather than the full screen.

### How do I prevent users from interacting with elements behind the spinner?

When using `Modal` with the default properties, user interaction is automatically blocked for elements behind the overlay. If you implement a custom absolute-fill `View` solution (such as for web), ensure the backdrop `View` has no `pointerEvents` prop set (defaults to `"auto"`), which captures all touch events. You can also add an `onStartShouldSetResponder` handler to the backdrop to explicitly capture gestures.

### Can I customize the animation when the spinner appears?

Yes. Change the **`animationType`** prop on the `Modal` component to `"fade"` or `"slide"` for built-in transitions. For more complex animations, wrap the `ActivityIndicator` in an `Animated.View` and use the `Animated` API to control opacity or scale when the `visible` prop changes. Note that the `animationType="none"` setting in the example above eliminates animation for immediate appearance.

### Is ActivityIndicator the best choice for all platforms?

**`ActivityIndicator`** is the recommended choice for standard loading states in React Native because it automatically renders the platform-native spinner (a spinning wheel on iOS, a circular progress indicator on Android). For custom branding or specific design requirements, you can replace `ActivityIndicator` with a custom animated component using `Animated` or `react-native-reanimated`, but this requires additional implementation for cross-platform consistency.