Skip to content

Spinner

Spinner provides a smooth, animated loading indicator with gradient visual effects and comprehensive customization options. Built with React Native Reanimated for optimal performance and fluid animations across all platforms.

import { Spinner } from 'rnc-theme';
<Spinner />
PropTypeDefaultDescription
sizeComponentSize | number'md'Spinner size (xs, sm, md, lg, xl) or custom pixel value
variantComponentVariant'default'Visual style variant
colorkeyof Theme['colors']-Custom color from theme colors
styleStyleProp<ViewStyle>-Additional container styles
thicknessnumber2Border thickness in pixels
durationnumber1000Animation duration in milliseconds
animatingbooleantrueWhether spinner should be visible and animating
SizePixelsUse Case
xs12pxSmall inline indicators
sm16pxButton loading states
md24pxStandard loading indicator
lg32pxCard or section loading
xl40pxFull screen loading
numberCustomPrecise sizing control
VariantDescriptionUse Case
defaultPrimary theme colorGeneral loading states
primaryPrimary brand colorMain action loading
secondarySecondary theme colorSecondary action loading
successSuccess state colorSuccess confirmation loading
errorError/danger colorError state loading
warningWarning state colorWarning action loading
infoInformation colorInfo action loading
destructiveDestructive action colorDelete/remove loading
outlineBorder theme colorSubtle loading indicator
filledFilled surface colorForm loading states
ghostTransparent primary colorOverlay loading states
const LoadingExample = () => {
const [loading, setLoading] = useState(false);
const handleAction = async () => {
setLoading(true);
try {
await performAsyncOperation();
} finally {
setLoading(false);
}
};
return (
<VStack spacing="lg" align="center">
<Button onPress={handleAction} disabled={loading}>
<ButtonText>Start Process</ButtonText>
</Button>
<Spinner animating={loading} variant="primary" size="lg" />
{loading && (
<Text>Processing your request...</Text>
)}
</VStack>
);
};
<VStack spacing="md" align="center" padding="lg">
<HStack spacing="lg" align="center">
<VStack align="center" spacing="sm">
<Spinner size="xs" variant="primary" />
<Text size="sm">Extra Small</Text>
</VStack>
<VStack align="center" spacing="sm">
<Spinner size="sm" variant="primary" />
<Text size="sm">Small</Text>
</VStack>
<VStack align="center" spacing="sm">
<Spinner size="md" variant="primary" />
<Text size="sm">Medium</Text>
</VStack>
<VStack align="center" spacing="sm">
<Spinner size="lg" variant="primary" />
<Text size="sm">Large</Text>
</VStack>
<VStack align="center" spacing="sm">
<Spinner size="xl" variant="primary" />
<Text size="sm">Extra Large</Text>
</VStack>
</HStack>
{/* Custom Size */}
<VStack align="center" spacing="sm">
<Spinner size={48} variant="primary" />
<Text size="sm">Custom 48px</Text>
</VStack>
</VStack>
<VStack spacing="md" padding="lg">
<Text variant="h3">Spinner Variants</Text>
<HStack spacing="md" wrap>
<VStack align="center" spacing="xs">
<Spinner variant="primary" />
<Text size="xs">Primary</Text>
</VStack>
<VStack align="center" spacing="xs">
<Spinner variant="secondary" />
<Text size="xs">Secondary</Text>
</VStack>
<VStack align="center" spacing="xs">
<Spinner variant="success" />
<Text size="xs">Success</Text>
</VStack>
<VStack align="center" spacing="xs">
<Spinner variant="error" />
<Text size="xs">Error</Text>
</VStack>
<VStack align="center" spacing="xs">
<Spinner variant="warning" />
<Text size="xs">Warning</Text>
</VStack>
<VStack align="center" spacing="xs">
<Spinner variant="info" />
<Text size="xs">Info</Text>
</VStack>
<VStack align="center" spacing="xs">
<Spinner variant="outline" />
<Text size="xs">Outline</Text>
</VStack>
<VStack align="center" spacing="xs">
<Spinner variant="ghost" />
<Text size="xs">Ghost</Text>
</VStack>
</HStack>
</VStack>
const ButtonWithSpinner = ({ loading, onPress, children }) => {
return (
<Button onPress={onPress} disabled={loading}>
<HStack spacing="sm" align="center">
{loading && <Spinner size="sm" color="background" />}
<ButtonText>{children}</ButtonText>
</HStack>
</Button>
);
};
// Usage
<VStack spacing="md">
<ButtonWithSpinner loading={isSubmitting} onPress={handleSubmit}>
Submit Form
</ButtonWithSpinner>
<ButtonWithSpinner loading={isUploading} onPress={handleUpload}>
Upload File
</ButtonWithSpinner>
<ButtonWithSpinner loading={isDeleting} onPress={handleDelete}>
Delete Item
</ButtonWithSpinner>
</VStack>
const LoadingOverlay = ({ visible, message }) => {
if (!visible) return null;
return (
<View style={StyleSheet.absoluteFillObject}>
<BlurView intensity={50} style={styles.overlay}>
<VStack spacing="lg" align="center">
<Spinner size="xl" variant="primary" />
{message && (
<Text color="primary" weight="medium">
{message}
</Text>
)}
</VStack>
</BlurView>
</View>
);
};
// Usage
const DataScreen = () => {
const [loading, setLoading] = useState(false);
return (
<View style={{ flex: 1 }}>
{/* Main content */}
<ScrollView>
{/* Your content here */}
</ScrollView>
<LoadingOverlay
visible={loading}
message="Loading data..."
/>
</View>
);
};
<VStack spacing="lg" padding="lg">
{/* Custom Colors */}
<Spinner
color="accent"
size="lg"
thickness={3}
/>
{/* Custom Duration */}
<Spinner
variant="success"
duration={2000}
size="lg"
/>
{/* Custom Container Style */}
<Spinner
variant="primary"
size="lg"
style={{
backgroundColor: 'rgba(0,0,0,0.1)',
borderRadius: 8,
padding: 16
}}
/>
{/* Ultra Thin Spinner */}
<Spinner
variant="outline"
size={60}
thickness={1}
duration={3000}
/>
</VStack>
const FadeSpinner = ({ visible, ...props }) => {
const opacity = useSharedValue(visible ? 1 : 0);
useEffect(() => {
opacity.value = withTiming(visible ? 1 : 0, {
duration: 200
});
}, [visible, opacity]);
const animatedStyle = useAnimatedStyle(() => ({
opacity: opacity.value
}));
return (
<Animated.View style={animatedStyle}>
<Spinner animating={visible} {...props} />
</Animated.View>
);
};
const DataList = () => {
const [data, setData] = useState([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [loadingMore, setLoadingMore] = useState(false);
const loadData = async (refresh = false) => {
if (refresh) {
setRefreshing(true);
} else {
setLoading(true);
}
try {
const newData = await fetchData();
setData(refresh ? newData : [...data, ...newData]);
} finally {
setLoading(false);
setRefreshing(false);
}
};
const loadMore = async () => {
setLoadingMore(true);
try {
const moreData = await fetchMoreData();
setData([...data, ...moreData]);
} finally {
setLoadingMore(false);
}
};
if (loading && data.length === 0) {
return (
<VStack flex={1} justify="center" align="center">
<Spinner size="xl" variant="primary" />
<Text color="muted">Loading data...</Text>
</VStack>
);
}
return (
<FlatList
data={data}
onRefresh={() => loadData(true)}
refreshing={refreshing}
onEndReached={loadMore}
renderItem={({ item }) => <DataItem item={item} />}
ListFooterComponent={
loadingMore ? (
<VStack padding="lg" align="center">
<Spinner size="md" variant="secondary" />
</VStack>
) : null
}
/>
);
};
const ValidatedInput = ({ onValidate, ...props }) => {
const [validating, setValidating] = useState(false);
const [isValid, setIsValid] = useState(null);
const handleValidation = async (value) => {
setValidating(true);
try {
const result = await onValidate(value);
setIsValid(result);
} finally {
setValidating(false);
}
};
return (
<VStack spacing="xs">
<HStack spacing="sm" align="center">
<TextInput
{...props}
onChangeText={handleValidation}
style={{ flex: 1 }}
/>
{validating && (
<Spinner size="sm" variant="info" />
)}
{!validating && isValid === true && (
<CheckIcon color="success" size={16} />
)}
{!validating && isValid === false && (
<XIcon color="error" size={16} />
)}
</HStack>
</VStack>
);
};
const MemoizedSpinner = React.memo(Spinner, (prevProps, nextProps) => {
return (
prevProps.animating === nextProps.animating &&
prevProps.variant === nextProps.variant &&
prevProps.size === nextProps.size
);
});
const SpinnerListItem = ({ loading, variant = 'primary' }) => {
if (!loading) return null;
return (
<View style={styles.listItemSpinner}>
<Spinner size="sm" variant={variant} />
</View>
);
};
const styles = StyleSheet.create({
listItemSpinner: {
position: 'absolute',
right: 16,
top: '50%',
transform: [{ translateY: -8 }]
}
});
const [isAnimating, setIsAnimating] = useState(false);
<VStack spacing="md" align="center">
<Spinner animating={isAnimating} size="lg" />
<Button onPress={() => setIsAnimating(!isAnimating)}>
<ButtonText>
{isAnimating ? 'Stop' : 'Start'} Animation
</ButtonText>
</Button>
</VStack>

Performance

  • Use animating={false} to hide spinners instead of conditional rendering
  • Avoid creating spinners inside frequently re-rendered components
  • Consider using React.memo for spinners with stable props

User Experience

  • Provide context with loading messages when appropriate
  • Use appropriate sizes for different UI contexts
  • Match spinner variants to the action being performed