Circular Loader
A circular loader rotates with optional gradient
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated react-native-svgCopy and paste the following code into your project.
component/molecules/circular-loader
import React, { useEffect, useRef } from "react";import { View, Animated, Easing } from "react-native";import Svg, { Circle, Defs, G, Filter, FeGaussianBlur } from "react-native-svg";import type { CircularLoaderProps } from "./types";export const CircularLoader: React.FC<CircularLoaderProps> = ({ size = 40, strokeWidth = 4, activeColor = "#000000", duration = 1000, style, gradientLength = 20, fadeOpacity = 0, blurAmount = 0, enableBlur = false,}) => { const rotateAnim = useRef(new Animated.Value(0)).current; const radius = (size - strokeWidth) / 2; const circumference = 2 * Math.PI * radius; const segments = 20; const gradientPercentage = gradientLength / 100; const activePercentage = 0.75; const solidArcPercentage = activePercentage - gradientPercentage; const segmentLength = (circumference * gradientPercentage) / segments; const activeLength = circumference * solidArcPercentage; useEffect(() => { Animated.loop( Animated.timing(rotateAnim, { toValue: 1, duration: duration, easing: Easing.linear, useNativeDriver: true, }), ).start(); }, [duration]); const rotate = rotateAnim.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "360deg"], }); return ( <View style={[{ width: size, height: size }, style]}> <Animated.View style={{ width: size, height: size, transform: [{ rotate }], }} > <Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}> {enableBlur && blurAmount > 0 && ( <Defs> <Filter id="motionBlurSmooth" x="-50%" y="-50%" width="200%" height="200%" > <FeGaussianBlur in="SourceGraphic" stdDeviation={blurAmount} /> </Filter> </Defs> )} <G rotation="-90" origin={`${size / 2}, ${size / 2}`}> <Circle cx={size / 2} cy={size / 2} r={radius} stroke={activeColor} strokeWidth={strokeWidth} strokeLinecap="round" fill="none" strokeDasharray={`${activeLength} ${circumference}`} filter={ enableBlur && blurAmount > 0 ? "url(#motionBlurSmooth)" : undefined } /> {/* Gradient segments - smooth fade using opacity interpolation */} {Array.from({ length: segments }).map((_, index) => { // Smooth exponential fade curve const progress = index / segments; const opacity = 1 - Math.pow(progress, 1.5); // Exponential curve for natural fade const finalOpacity = opacity * (1 - fadeOpacity) + fadeOpacity; const offset = -(activeLength + segmentLength * index); // Don't render segments that are too faint to avoid visible dots if (finalOpacity < 0.01) { return null; } return ( <Circle key={index} cx={size / 2} cy={size / 2} r={radius} stroke={activeColor} strokeWidth={strokeWidth} strokeLinecap="butt" fill="none" opacity={finalOpacity} strokeDasharray={`${segmentLength} ${circumference}`} strokeDashoffset={offset} filter={ enableBlur && blurAmount > 0 ? "url(#motionBlurSmooth)" : undefined } /> ); })} </G> </Svg> </Animated.View> </View> );};Usage
import { View, Text, StyleSheet } from "react-native";import { GestureHandlerRootView } from "react-native-gesture-handler";import { StatusBar } from "expo-status-bar";import { useFonts } from "expo-font";import { SymbolView } from "expo-symbols";import { DisclosureGroup } from "@/components/molecules/disclosure-group";import DynamicText from "@/components/molecules/dynamic-text";import { DynamicTextItem } from "@/components/molecules/dynamic-text/types";import GooeyText from "@/components/molecules/gooey-text";import { CircleLoadingIndicator } from "@/components";import { CircularLoader } from "@/components/molecules/Loaders/circular";export default function App() { const [fontLoaded] = useFonts({ SfProRounded: require("@/assets/fonts/sf-pro-rounded.ttf"), HelveticaNowDisplay: require("@/assets/fonts/HelveticaNowDisplayMedium.ttf"), }); const OPTIONS = [ { label: "Edit", icon: "pencil" }, { label: "Duplicate", icon: "doc.on.doc" }, { label: "Share", icon: "square.and.arrow.up" }, { label: "Delete", icon: "trash", destructive: true }, ]; const GOOEY_TEXTS: string[] = ["REACTICX", "IS", "AWESOME!"]; return ( <GestureHandlerRootView style={styles.container}> <StatusBar style="light" /> <View style={styles.content}> <CircularLoader activeColor="#fff" gradientLength={50} enableBlur /> </View> </GestureHandlerRootView> );}const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: "#0a0a0a", }, content: { paddingHorizontal: 20, paddingTop: 90, justifyContent: "center", alignItems: "center", gap: 0, }, title: { fontSize: 28, fontWeight: "700", color: "#fff", }, subtitle: { fontSize: 15, color: "#555", }, card: { backgroundColor: "#141414", borderRadius: 16, overflow: "hidden", marginTop: 20, }, triggerContent: { padding: 16, }, triggerLeft: { flexDirection: "row", alignItems: "center", gap: 12, }, triggerText: { fontSize: 16, fontWeight: "500", color: "#fff", }, item: { flexDirection: "row", alignItems: "center", gap: 12, padding: 14, backgroundColor: "#1a1a1a", borderRadius: 12, marginBottom: 6, }, itemText: { fontSize: 15, color: "#fff", }, destructiveText: { color: "#ff453a", },});Props
React Native Reanimated
React Native Svg
