Circle Loader
Three dots animating in a wave pattern.
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/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
