Progress
An animated progress bar that smoothly fills based on progress with optional gradient
Last updated on
Manual
Install the following dependencies:
npm install react-native-reanimated expo-linear-gradientCopy and paste the following code into your project.
component/organisms/progress
import React, { useEffect } from "react";import { StyleSheet, View, Dimensions, Text } from "react-native";import Animated, { useSharedValue, useAnimatedStyle, withTiming, Easing, withRepeat, withSequence,} from "react-native-reanimated";import { LinearGradient } from "expo-linear-gradient";import type { AnimatedProgressBarProps } from "./AnimatedProgress.types";export const AnimatedProgressBar: React.FC<AnimatedProgressBarProps> = ({ progress = 0, animationDuration = 800, width = "100%", height = 10, progressColor = "#2089dc", trackColor = "#e0e0e0", borderRadius = 4, showPercentage = false, percentagePosition = "right", percentageTextStyle, containerStyle, formatPercentage = (value: number) => `${Math.round(value * 100)}%`, indeterminate = false, pulsate = false, onAnimationComplete, gradientColors = ["#4dabf7", "#3b5bdb"], useGradient = false,}) => { const validProgress = Math.min(Math.max(progress, 0), 1); const progressValue = useSharedValue(0); const indeterminateValue = useSharedValue(0); const pulseValue = useSharedValue(1); const screenWidth = Dimensions.get("window").width; const containerWidth = typeof width === "string" ? width.endsWith("%") ? screenWidth * (parseInt(width, 10) / 100) : parseInt(width, 10) : width; useEffect(() => { if (!indeterminate) { progressValue.value = withTiming( validProgress, { duration: animationDuration, easing: Easing.bezier(0.25, 0.1, 0.25, 1), }, (isFinished) => { if (isFinished && onAnimationComplete) { onAnimationComplete(); } }, ); } }, [validProgress, animationDuration, onAnimationComplete, indeterminate]); useEffect(() => { if (indeterminate) { indeterminateValue.value = 0; indeterminateValue.value = withRepeat( withTiming(1, { duration: 1500, easing: Easing.linear }), -1, false, ); } else { indeterminateValue.value = 0; } return () => { indeterminateValue.value = 0; }; }, [indeterminate]); useEffect(() => { if (pulsate && !indeterminate && validProgress > 0) { pulseValue.value = withRepeat( withSequence( withTiming(1.1, { duration: 500, easing: Easing.ease }), withTiming(1, { duration: 500, easing: Easing.ease }), ), -1, true, ); } else { pulseValue.value = 1; } return () => { pulseValue.value = 1; }; }, [pulsate, indeterminate, validProgress]); const progressBarStyle = useAnimatedStyle(() => { if (indeterminate) { return { position: "absolute", left: 0, top: 0, width: "30%", height: "100%", backgroundColor: useGradient ? "transparent" : progressColor, borderRadius, transform: [ { translateX: withRepeat( withTiming(containerWidth * 0.7, { duration: 1500, easing: Easing.linear, }), -1, true, ), }, ], }; } return { width: `${progressValue.value * 100}%`, backgroundColor: useGradient ? "transparent" : progressColor, borderRadius, height: "100%", transform: pulsate ? [{ scale: pulseValue.value }] : [], }; }); const renderPercentageText = () => { if (!showPercentage) return null; const textContent = formatPercentage(validProgress); const textElement = ( <Text style={[styles.percentageText, percentageTextStyle]}> {textContent} </Text> ); if (percentagePosition === "inside" && validProgress > 0.1) { return <View style={styles.insideTextContainer}>{textElement}</View>; } return textElement; }; const renderProgressBar = () => { if (useGradient) { return ( <Animated.View style={progressBarStyle}> <LinearGradient colors={gradientColors as [string, string, ...string[]]} start={{ x: 0, y: 0 }} end={{ x: 1, y: 0 }} style={styles.gradient} > {percentagePosition === "inside" && renderPercentageText()} </LinearGradient> </Animated.View> ); } return ( <Animated.View style={progressBarStyle}> {percentagePosition === "inside" && renderPercentageText()} </Animated.View> ); }; return ( <View style={[ styles.container, { flexDirection: percentagePosition === "left" || percentagePosition === "right" ? "row" : "column", }, containerStyle, ]} > {percentagePosition === "left" && renderPercentageText()} {percentagePosition === "top" && renderPercentageText()} <View style={[ styles.track, { width: width, height: height, backgroundColor: trackColor, borderRadius, overflow: "hidden", } as any, ]} > {!indeterminate ? renderProgressBar() : renderProgressBar()} </View> {percentagePosition === "right" && renderPercentageText()} {percentagePosition === "bottom" && renderPercentageText()} </View> );};const styles = StyleSheet.create({ container: { alignItems: "center", justifyContent: "center", }, track: { justifyContent: "center", position: "relative", }, percentageText: { fontSize: 12, fontWeight: "bold", marginHorizontal: 8, color: "#000", }, insideTextContainer: { position: "absolute", top: 0, left: 0, right: 0, bottom: 0, justifyContent: "center", alignItems: "center", }, gradient: { flex: 1, borderRadius: 4, },});Usage
import { StyleSheet, Text, View } from "react-native";import React from "react";import { StackCards } from "@/components/molecules/stack-carousel";import { AnimatedProgressBar } from "@/components";const App = () => { return <AnimatedProgressBar progress={2} />;};export default App;const styles = StyleSheet.create({});Props
React Native Reanimated
Expo Linear Gradient
