Circular Progress
Animated SVG progress ring with gradient stroke and count-up number
animationprogressringsvgdata-viz
Install dependencies
$npm install framer-motionPreview
Source Code
"use client";
import { useEffect, useId } from "react";
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion";
interface CircularProgressProps {
value?: number;
size?: number;
strokeWidth?: number;
colors?: [string, string];
showValue?: boolean;
className?: string;
}
function CountUp({ value }: { value: import("framer-motion").MotionValue<number> }) {
const rounded = useTransform(value, (v) => Math.round(v));
return <motion.span>{rounded}</motion.span>;
}
export default function CircularProgress({
value = 0,
size = 120,
strokeWidth = 8,
colors = ["#6366f1", "#ec4899"],
showValue = true,
className = "",
}: CircularProgressProps) {
const gradientId = useId();
const radius = (size - strokeWidth) / 2;
const circumference = 2 * Math.PI * radius;
const motionValue = useMotionValue(0);
const spring = useSpring(motionValue, { damping: 20, stiffness: 80 });
const dashOffset = useTransform(spring, (v) => circumference * (1 - v / 100));
useEffect(() => {
motionValue.set(value);
}, [value, motionValue]);
return (
<div
className={`relative inline-flex items-center justify-center ${className}`}
style={{ width: size, height: size }}
>
<svg
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
className="-rotate-90"
>
<defs>
<linearGradient id={gradientId} x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stopColor={colors[0]} />
<stop offset="100%" stopColor={colors[1]} />
</linearGradient>
</defs>
{/* Background track */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke="rgba(255,255,255,0.08)"
strokeWidth={strokeWidth}
/>
{/* Progress arc */}
<motion.circle
cx={size / 2}
cy={size / 2}
r={radius}
fill="none"
stroke={`url(#${gradientId})`}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray={circumference}
style={{ strokeDashoffset: dashOffset }}
/>
</svg>
{showValue && (
<div className="absolute inset-0 flex items-center justify-center">
<span className="text-2xl font-bold text-white">
<CountUp value={spring} />
<span className="text-sm text-gray-400">%</span>
</span>
</div>
)}
</div>
);
}