Circular Loader

A circular loader rotates with optional gradient

Last updated on

Edit on GitHub

Manual

Install the following dependencies:

npm install react-native-reanimated react-native-svg

Copy 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