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

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) 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) (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) (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.

// 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.

// 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) 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.
  • 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.

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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →