Circle Loader

Three dots animating in a wave pattern.

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/circle-loader

// components/LoadingIndicator.tsximport React, { useEffect } from "react";import { View, StyleSheet, ViewStyle } from "react-native";import Svg, { Circle, CircleProps } from "react-native-svg";import Animated, {  useSharedValue,  useAnimatedProps,  withRepeat,  withTiming,  Easing,} from "react-native-reanimated";import { ICircleLoadingIndicator } from "./Circle.types";const AnimatedCircle = Animated.createAnimatedComponent(Circle);export const CircleLoadingIndicator: React.FC<ICircleLoadingIndicator> = ({  dotColor = "#007AFF",  dotRadius = 6,  dotSpacing = 20,  duration = 950,  style,}) => {  const progress1 = useSharedValue<number>(0);  const progress2 = useSharedValue<number>(0);  const progress3 = useSharedValue<number>(0);  useEffect(() => {    progress1.value = withRepeat(      withTiming(1, {        duration,        easing: Easing.inOut(Easing.ease),      }),      -1,      true,    );    setTimeout(() => {      progress2.value = withRepeat(        withTiming<number>(1, {          duration,          easing: Easing.inOut(Easing.ease),        }),        -1,        true,      );    }, duration / 3);    setTimeout(      () => {        progress3.value = withRepeat(          withTiming<number>(1, {            duration,            easing: Easing.inOut(Easing.ease),          }),          -1,          true,        );      },      (2 * duration) / 3,    );  }, [duration]);  const jumpHeight = dotRadius * 0.85;  const animatedProps1 = useAnimatedProps<    Required<Partial<Pick<CircleProps, "cy">>>  >(() => ({    cy: 12 - progress1.value * jumpHeight,  }));  const animatedProps2 = useAnimatedProps(() => ({    cy: 12 - progress2.value * jumpHeight,  }));  const animatedProps3 = useAnimatedProps(() => ({    cy: 12 - progress3.value * jumpHeight,  }));  return (    <View style={[styles.container, style]}>      <Svg width={(dotRadius * 2 + dotSpacing) * 3} height={24}>        <AnimatedCircle          cx={dotRadius}          cy={12}          r={dotRadius}          fill={dotColor}          animatedProps={animatedProps1}        />        <AnimatedCircle          cx={dotRadius + dotSpacing + dotRadius * 2}          cy={12}          r={dotRadius}          fill={dotColor}          animatedProps={animatedProps2}        />        <AnimatedCircle          cx={dotRadius + (dotSpacing + dotRadius * 2) * 2}          cy={12}          r={dotRadius}          fill={dotColor}          animatedProps={animatedProps3}        />      </Svg>    </View>  );};const styles = StyleSheet.create({  container: {    height: 40,    justifyContent: "center",    alignItems: "center",  },});

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";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}>        <CircleLoadingIndicator          dotSpacing={8}          dotColor="#fff"          style={{            marginTop: 60,          }}          duration={500}        />      </View>    </GestureHandlerRootView>  );}const styles = StyleSheet.create({  container: {    flex: 1,    backgroundColor: "#0a0a0a",  },  content: {    paddingHorizontal: 20,    paddingTop: 50,    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