What You'll Learn
Loading states are a critical part of any mobile application's user experience. When users interact with your app, they often need to wait for data to load, computations to complete, or network requests to finish. The React Native ActivityIndicator component provides a standardized way to communicate these waiting periods to users, but default indicators often fall short of matching your application's visual identity.
In this comprehensive guide, you'll discover:
- How to configure the built-in ActivityIndicator component
- Techniques for building fully custom animated loaders
- Dark mode and theming integration strategies
- Performance optimization for smooth animations
- Accessibility considerations for inclusive loading experiences
- Real-world implementation patterns and best practices
Whether you're building a simple spinner or a complex animated loader, these techniques will help you implement loading states that feel native to your application while providing clear feedback to users during asynchronous operations.
For mobile development services that prioritize user experience, explore our mobile app development expertise.
Everything you need to know about React Native activity indicators
Core ActivityIndicator Props
Master animating, color, size, and platform-specific props for the built-in component.
Custom Animation Development
Build unique loading indicators using React Native's Animated API with full creative control.
Theming & Dark Mode
Create indicators that adapt seamlessly to light and dark theme preferences.
Performance Optimization
Achieve smooth 60fps animations using native driver and proper cleanup techniques.
Accessibility Implementation
Support screen readers, reduced motion preferences, and color blindness.
Third-Party Libraries
Evaluate and integrate community-maintained loading indicator solutions.
Understanding the React Native ActivityIndicator Component
The ActivityIndicator component is React Native's built-in solution for displaying loading indicators to users. It renders a circular spinning animation that communicates ongoing activity without requiring users to understand technical details about what's happening behind the scenes. The component is part of React Native's core component library, meaning it works consistently across both iOS and Android platforms without requiring additional dependencies or configuration.
At its most basic level, the ActivityIndicator requires no configuration at all. Simply importing and rendering the component provides a functional loading spinner that users immediately recognize as indicating a waiting state. This built-in accessibility and recognition makes the component valuable for quick implementations, but its default appearance rarely aligns with modern application design systems. The component accepts several props that modify its behavior and appearance, providing a middle ground between the default spinner and a completely custom implementation.
The ActivityIndicator is designed to be semantically meaningful, with screen readers automatically recognizing it as a loading indicator. This built-in accessibility support means that users relying on assistive technologies receive appropriate feedback about ongoing activities without requiring additional implementation effort. The component also handles animation timing and performance optimization internally, ensuring smooth visual feedback even during extended loading periods or on lower-powered devices.
Core ActivityIndicator Props and Configuration
The ActivityIndicator component exposes several props that control its appearance and behavior. Understanding these props enables you to customize loading indicators without requiring custom implementations.
animating
The animating prop determines whether the indicator is currently active, with a default value of true. Setting this prop to false hides the indicator entirely, which is useful when you want to conditionally display loading states based on application state. This prop works seamlessly with React's state management, allowing you to control loading indicators through local component state or global application state.
color
The color prop controls the spinner's tint color, accepting color values as strings in various formats including hex codes, RGB values, and named colors. On iOS, the default color uses the system's accent color, which adapts to the user's system-wide color settings. On Android, the default color is a neutral gray (#999999). This platform difference means that explicitly setting a color ensures visual consistency across both platforms.
size
The size prop accepts either string values of 'small' or 'large', or numeric values for precise control over the indicator's dimensions. The small size renders at approximately 20 points, while the large size renders at approximately 50 points on both platforms. When using numeric values, you specify the exact size in points, giving you pixel-perfect control.
hidesWhenStopped (iOS)
On iOS specifically, the hidesWhenStopped prop controls whether the indicator becomes invisible when the animating prop is set to false. This prop defaults to true on iOS, meaning a non-animating ActivityIndicator occupies no visual space. On Android, this prop has no effect.
1import React, { useState, useEffect } from 'react';2import { ActivityIndicator, StyleSheet, View, Text } from 'react-native';3 4const LoadingExample = () => {5 const [isLoading, setIsLoading] = useState(true);6 7 useEffect(() => {8 // Simulate loading process9 setTimeout(() => setIsLoading(false), 3000);10 }, []);11 12 return (13 <View style={styles.container}>14 {isLoading ? (15 <ActivityIndicator16 animating={isLoading}17 size="large"18 color="#6C63FF"19 />20 ) : (21 <Text>Content loaded!</Text>22 )}23 </View>24 );25};26 27const styles = StyleSheet.create({28 container: {29 flex: 1,30 justifyContent: 'center',31 alignItems: 'center',32 },33});Building Custom Activity Indicators from Scratch
While the built-in ActivityIndicator provides a solid foundation, many applications require loading indicators that align more closely with their design language. Building a custom activity indicator from scratch using React Native's Animated API gives you complete control over the visual appearance, animation timing, and behavior of your loading indicators.
The Animated API provides the foundation for smooth, performant animations in React Native. For a basic custom activity indicator, you create one or more animated values that drive the rotation, opacity, or scale of view elements. The simplest custom indicator consists of one or more rotating circles or lines that animate continuously to create the illusion of activity.
Custom indicators often incorporate multiple visual elements that animate in coordination. A common pattern uses several dots or lines that fade in and out in sequence, creating a "marching ants" effect that clearly communicates ongoing activity. This approach works particularly well for loading states that need to fit into narrow horizontal or vertical spaces where a circular spinner would feel awkward.
By composing multiple animated values and views, you can create complex loading patterns that feel unique to your application. The timing and easing functions applied to these animations significantly impact the perceived quality of the loading experience, with carefully chosen easing creating more natural, pleasing motion. For more advanced animation techniques, explore our guide to React Native state patterns that complement animated loading states.
Creating a Custom Dot Loader
This implementation creates a four-dot loader that animates in sequence using Animated.sequence and Animated.stagger. The useRef hook maintains stable animation references across renders, while the useEffect hook sets up the looping animation with proper cleanup.
The animation creates a pulsing effect where each dot fades and scales independently, with staggered timing creating a wave-like motion across the dots. This pattern is commonly used for loading states in chat interfaces, content feeds, and compact loading areas.
Key implementation details include:
- Using useRef for stable animation value references
- Implementing proper cleanup in the effect return function
- Using useNativeDriver for smooth performance
- Creating staggered animations with Animated.stagger
The cleanup function is essential for preventing memory leaks and unnecessary CPU usage when the component unmounts or when dependencies change. Without proper cleanup, animations would continue running in the background, degrading application performance.
1import React, { useRef, useEffect } from 'react';2import { Animated, View, StyleSheet } from 'react-native';3 4const CustomActivityIndicator = ({5 size = 40,6 color = '#6C63FF',7 duration = 12008}) => {9 const animations = useRef(10 Array(4).fill(0).map(() => new Animated.Value(0))11 ).current;12 13 useEffect(() => {14 const createAnimation = (index) => {15 return Animated.sequence([16 Animated.delay(index * (duration / 4)),17 Animated.timing(animations[index], {18 toValue: 1,19 duration: duration / 4,20 useNativeDriver: true,21 }),22 Animated.timing(animations[index], {23 toValue: 0,24 duration: duration / 4,25 useNativeDriver: true,26 }),27 ]);28 };29 30 const animationsList = animations.map((_, index) =>31 createAnimation(index)32 );33 34 Animated.loop(35 Animated.stagger(duration / 4, animationsList)36 ).start();37 38 return () => {39 animations.forEach(anim => anim.stopAnimation());40 };41 }, [animations, duration]);42 43 return (44 <View style={[styles.container, { width: size, height: size }]}>45 {animations.map((animValue, index) => (46 <Animated.View47 key={index}48 style={[49 styles.dot,50 {51 backgroundColor: color,52 opacity: animValue,53 transform: [{54 scale: animValue.interpolate({55 inputRange: [0, 1],56 outputRange: [0.5, 1],57 })58 }],59 },60 ]}61 />62 ))}63 </View>64 );65};66 67const styles = StyleSheet.create({68 container: {69 flexDirection: 'row',70 justifyContent: 'space-between',71 alignItems: 'center',72 },73 dot: {74 width: 10,75 height: 10,76 borderRadius: 5,77 },78});Theming and Dark Mode Integration
Modern applications typically implement comprehensive theming systems that control colors, typography, and spacing throughout the interface. Loading indicators should integrate seamlessly with these theming systems to maintain visual consistency. Rather than hardcoding colors or sizes, custom indicators should accept theme values as props or read from a centralized theme context.
React Native's useColorScheme hook detects the current color scheme preference, enabling automatic adaptation to light or dark mode. This hook returns 'light' or 'dark' based on the user's system settings, allowing your loading indicators to automatically adjust their colors when the system theme changes. Building indicators that automatically adapt to these preferences creates a more polished user experience.
For applications with custom theme systems, wrapping your indicator in a theme consumer or using a theme hook enables automatic color adaptation based on the current theme state. This approach ensures that loading indicators maintain visual consistency with other interface elements as themes change, whether through user preference or system-wide updates.
The useColorScheme hook works on both iOS and Android, though the specific mechanism for detecting system preferences differs between platforms. React Native abstracts these differences, providing a unified API for theme detection that works consistently across the React Native ecosystem.
Learn more about CSS techniques and font integration for consistent theming across your web and mobile applications.
1import React, { useColorScheme } from 'react';2import { View, StyleSheet } from 'react-native';3 4const ThemedActivityIndicator = ({5 size = 'large',6 customColor7}) => {8 const colorScheme = useColorScheme();9 10 const getIndicatorColor = () => {11 if (customColor) return customColor;12 return colorScheme === 'dark'13 ? '#FFFFFF'14 : '#6C63FF';15 };16 17 return (18 <ActivityIndicator19 size={size}20 color={getIndicatorColor()}21 />22 );23};24 25const ThemedCustomIndicator = ({26 theme = { primary: '#6C63FF', background: '#FFFFFF' }27}) => {28 const scheme = useColorScheme();29 const colors = scheme === 'dark'30 ? { primary: '#9B8AFF', background: '#1A1A1A' }31 : theme;32 33 return (34 <CustomActivityIndicator color={colors.primary} />35 );36};Advanced Animation Techniques for Loading States
Beyond basic spinning and pulsing animations, advanced loading indicators can incorporate more complex motion patterns that convey specific types of activities or progress. Understanding animation principles helps you create loading experiences that feel purposeful and engaging rather than generic.
Progress-Based Indicators
Progress-based loading indicators communicate not just that something is happening, but how much of the task is complete. These indicators use animated values that map directly to progress percentages, creating a clear visual representation of completion status. Implementing progress indicators requires managing animation state that corresponds to actual application progress, whether that's bytes downloaded, items processed, or steps completed in a multi-stage operation.
Easing Functions
Easing functions dramatically impact how animations feel to users. Linear animations can feel mechanical and unpolished, while carefully chosen easing functions create more natural, pleasing motion. The most common easing for loading animations is slightly accelerated or decelerated motion, which mimics natural physical movement. React Native's Easing module provides several built-in easing functions including:
- Easing.linear - Constant speed animation
- Easing.ease - Smooth acceleration and deceleration
- Easing.quad - Quadratic easing curve
- Easing.cubic - Cubic easing curve
- Easing.inOut - Smooth start and end with faster middle
Compound Animations
Complex loading indicators often combine multiple animation types to create sophisticated visual effects. Animated.parallel runs animations simultaneously, while Animated.sequence plays them in order. Animated.stagger provides the best of both worlds, running animations concurrently but with staggered start times. Combining these techniques creates loading indicators with depth and visual interest.
Performance Optimization for Loading Indicators
Loading indicators run continuously during potentially long-running operations, making performance optimization essential for maintaining smooth application behavior. Poorly implemented animations can cause frame drops, battery drain, and degraded user experience.
Native Driver Optimization
The useNativeDriver option in React Native's Animated API is critical for achieving smooth animations. When enabled, animations run on the native rendering thread, bypassing the JavaScript thread entirely. This eliminates synchronization issues between JavaScript execution and UI updates, resulting in significantly smoother animations. For loading indicators that animate continuously, enabling native driver support is essential for maintaining 60fps animation performance.
Animation Cleanup
Components that use animations should implement proper cleanup in their effect cleanup functions or component unmount handlers. Unmanaged animations continue running even when their containing component is no longer visible, wasting CPU and GPU resources. Using the cleanup functions provided by Animated ensures that animations stop when they're no longer needed, preserving battery life and application performance.
Memory Management
Creating new Animated.Value instances on every render can lead to memory leaks and degraded performance over time. Using useRef to maintain stable animation references and only creating new values when necessary keeps memory usage stable. For applications with many simultaneous loading states, consider implementing a single shared animation value that multiple indicators can reference.
1import React, { useRef, useEffect, useCallback } from 'react';2import { Animated, StyleSheet } from 'react-native';3 4const OptimizedLoader = ({5 isLoading,6 children,7 loaderComponent: LoaderComponent8}) => {9 const opacityAnim = useRef(new Animated.Value(isLoading ? 1 : 0)).current;10 const scaleAnim = useRef(new Animated.Value(1)).current;11 12 const animateTransition = useCallback((showLoader) => {13 Animated.parallel([14 Animated.timing(opacityAnim, {15 toValue: showLoader ? 1 : 0,16 duration: 200,17 useNativeDriver: true,18 }),19 Animated.spring(scaleAnim, {20 toValue: showLoader ? 1 : 0.95,21 useNativeDriver: true,22 }),23 ]).start();24 }, [opacityAnim, scaleAnim]);25 26 useEffect(() => {27 animateTransition(isLoading);28 }, [isLoading, animateTransition]);29 30 return (31 <Animated.View32 style={[33 styles.container,34 {35 opacity: opacityAnim,36 transform: [{ scale: scaleAnim }]37 }38 ]}39 >40 {isLoading ? <LoaderComponent /> : children}41 </Animated.View>42 );43};Accessibility Considerations for Loading Indicators
Accessibility in loading indicators extends beyond simple screen reader compatibility. Well-designed loading experiences accommodate users with various abilities and preferences, including those who may have difficulty perceiving motion, distinguishing certain colors, or understanding non-standard loading patterns.
Screen Reader Support
The ActivityIndicator component includes built-in accessibility support that announces loading states appropriately. For custom indicators, implementing accessibility features using React Native's Accessibility API ensures that users with visual impairments receive equivalent information about loading progress. Setting appropriate accessibility labels and roles helps users understand what's happening during loading sequences.
Reduced Motion Preferences
Motion sensitivity affects a significant portion of application users, with some experiencing discomfort or disorientation from animated interfaces. React Native's reduced motion preference detection allows you to respect users' system-level motion preferences. When reduced motion is enabled, consider replacing continuous animations with static indicators or significantly reducing animation intensity.
Color Accessibility
Color accessibility considerations ensure that loading indicators remain visible and distinguishable for users with various forms of color vision deficiency. Using colors with sufficient contrast against their background, providing non-color indicators of activity (such as motion or text labels), and testing indicators with color blindness simulation tools helps ensure accessibility. Loading indicators that rely solely on color changes to convey meaning may be ineffective for users who cannot distinguish those colors.
Accessibility Best Practices
- Use
accessibilityRole="progressbar"to identify loading indicators - Provide meaningful
accessibilityLabelvalues that describe what's loading - Use
accessibilityStateto communicate busy/in-progress states - Implement
accessibilityValuefor progress-based indicators with percentage completion - Respect reduced motion preferences using useAccessibilityInfo
Third-Party Libraries for Enhanced Loading Indicators
The React Native ecosystem includes several well-maintained libraries that provide enhanced loading indicator components. These libraries offer animations and features beyond what's available in the core ActivityIndicator, often with minimal configuration required.
react-native-indicators
This library provides a comprehensive collection of pre-built loading animations including various spinner styles, bars, dots, and custom animations. The library is designed for easy integration, accepting props that control colors, sizes, and animation speeds. For projects that need multiple loading indicator styles or don't want to invest in building custom animations, this library offers a mature, well-tested solution.
react-native-spinkit
This library focuses on providing iOS-style loading indicators with smooth, native-feeling animations. The library includes multiple spinner types modeled after Apple's Human Interface Guidelines loading indicators. For applications targeting iOS users primarily, or for teams that want their loading indicators to feel native to the Apple platform, this library provides appropriate visual options.
Evaluation Criteria
When evaluating third-party libraries, consider:
- Maintenance activity and community support
- Compatibility with your React Native version
- Bundle size impact on application
- License requirements for commercial use
- Customization flexibility for your use case
For critical UI elements like loading indicators, either investing in custom implementations or choosing libraries with strong, active maintenance communities protects your application from future compatibility issues.
react-native-indicators
Comprehensive collection of loading animations including bezier Circles, dot promotions, and wave effects.
react-native-spinkit
iOS-style spinners with 13 different animation types modeled after Apple's HIG.
react-native-animated-loader
SVG-based animated loader with customizable colors, sizes, and animation speed.
rn-spinner
Lightweight loading spinner with no external image dependencies and native feel.
Implementing Loading States in Real-World Applications
Loading states in real applications often involve complex state management, multiple concurrent operations, and varying degrees of progress. Designing loading indicator systems that handle these complexities gracefully requires thoughtful architecture and clear patterns.
Compound Loading States
Compound loading states occur when multiple operations need to complete before content can display. Managing these states often involves tracking individual operation statuses and deriving the overall loading state from those statuses. This approach provides flexibility for displaying partial content when possible while indicating when additional operations remain. For example, a dashboard might load cached data immediately while fetching fresh data in the background.
Error Handling During Loading
When loading fails, users should understand what happened and what options they have. Rather than simply removing the loading indicator, implement error states that provide clear information and retry options. This approach maintains user trust and provides paths forward when unexpected issues occur during loading operations.
Custom Hooks for Loading State
Creating custom hooks for loading state management encapsulates the logic for fetching, loading, and error states in reusable components. This pattern keeps your UI components clean and focused on presentation while centralizing loading state logic. The hook pattern also makes it easy to add features like automatic retry, timeout handling, or caching.
Discover more web development techniques to build robust, user-friendly applications.
Custom Hook for Async Data Loading
This custom hook encapsulates the loading state management for async data fetching operations. It provides data, loading status, and error state along with a refetch function for manual retry. The hook handles the complete lifecycle of async operations including initial load, success, and error states.
Using this hook in your components keeps loading state logic separate from presentation logic, making components easier to read and maintain. The hook pattern also enables easy sharing of loading logic across multiple components that need to fetch similar data.
The DataListWithLoader component demonstrates how to use this hook while providing appropriate UI states for loading, error, empty, and success conditions. Each state displays relevant information and actions, keeping users informed throughout the data fetching process. For more on React hooks and state management, explore our guide to React useReducer.
1import React, { useState, useEffect, useMemo } from 'react';2import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';3 4const useAsyncData = (fetchFunction, dependencies = []) => {5 const [data, setData] = useState(null);6 const [isLoading, setIsLoading] = useState(true);7 const [error, setError] = useState(null);8 9 const refetch = async () => {10 setIsLoading(true);11 setError(null);12 try {13 const result = await fetchFunction();14 setData(result);15 } catch (err) {16 setError(err);17 } finally {18 setIsLoading(false);19 }20 };21 22 useEffect(() => {23 refetch();24 }, dependencies);25 26 return { data, isLoading, error, refetch };27};28 29const DataListWithLoader = ({ fetchData, renderItem, emptyMessage }) => {30 const { data, isLoading, error, refetch } = useAsyncData(fetchData);31 32 if (isLoading) {33 return (34 <View style={styles.loadingContainer}>35 <ActivityIndicator size="large" color="#6C63FF" />36 <Text style={styles.loadingText}>Loading data...</Text>37 </View>38 );39 }40 41 if (error) {42 return (43 <View style={styles.errorContainer}>44 <Text style={styles.errorText}>Unable to load content.</Text>45 <TouchableOpacity style={styles.retryButton} onPress={refetch}>46 <Text style={styles.retryButtonText}>Retry</Text>47 </TouchableOpacity>48 </View>49 );50 }51 52 if (!data || data.length === 0) {53 return (54 <View style={styles.emptyContainer}>55 <Text style={styles.emptyText}>{emptyMessage}</Text>56 </View>57 );58 }59 60 return data.map(renderItem);61};Best Practices for Activity Indicator Implementation
Implementing effective loading indicators involves balancing visual appeal, performance, and user experience considerations. Following established best practices helps create loading experiences that serve users well while minimizing development and maintenance overhead.
Consistency Across Your Application
Using the same loading indicator style throughout your application helps users immediately recognize loading states regardless of where they encounter them. Define loading indicator components or styles once and reuse them across your application rather than implementing inline loading indicators in individual screens. This consistency also makes it easy to update the loading appearance across your entire application when requirements change.
Appropriate Positioning
Loading indicators should appear in locations that clearly indicate which content is loading, typically replacing the content that will load. When loading additional content, indicators should appear adjacent to the area where new content will insert, maintaining spatial context for users. Center loading indicators within the content area they replace for clear visual correspondence.
Timing Considerations
Immediate display of loading indicators (within 100ms of starting an operation) prevents users from wondering whether their action was registered. For very quick operations (under 1 second), consider whether a loading indicator is necessary or whether the operation can complete without explicit loading feedback. Instant transitions for quick operations often feel more responsive than showing brief loading indicators.
Summary of Key Takeaways
-
Start with the built-in ActivityIndicator for simple loading needs, customizing color and size to match your brand.
-
Build custom indicators using the Animated API when you need unique visual experiences or specific animation patterns.
-
Integrate with your theming system using useColorScheme for automatic light/dark mode support.
-
Always enable useNativeDriver for smooth 60fps animations and implement proper cleanup.
-
Implement accessibility features including screen reader support and reduced motion preferences.
-
Use custom hooks to encapsulate loading state management logic for cleaner components.
-
Design for all states including loading, success, error, and empty states with appropriate user feedback.
-
Test across devices to ensure loading indicators perform well on lower-powered devices and across different screen sizes.