Back to Dictionary
Status & Confirmation
●○○Ready to Use
Recommended defaultProgress Ring
A circular SVG ring that visually fills its stroke perimeter based on a percentage value, moving smoothly as progress updates.
Use
File uploads where exact percentage is known but horizontal space is tight
Avoid
Indeterminate waiting states (use Spinner instead)
Safer Alternative
No safer default listed.
Risk Level
Low risk
Live Demo
0%
Uploading file...
What It Is
A circular SVG ring that visually fills its stroke perimeter based on a percentage value, moving smoothly as progress updates.
✓When to Use
- File uploads where exact percentage is known but horizontal space is tight
- Daily goal completions (e.g., fitness rings)
- Time remaining in a countdown
✕When NOT to Use
- Indeterminate waiting states (use Spinner instead)
Configuration Tips
- 01Calculate the SVG circle circumference (2 * Math.PI * radius) and animate the stroke-dashoffset
- 02Add a smooth transition class to the circle so it glides between percentages
You've Seen It In
Apple FitnessVercel DeploymentLinear issues
AI Implementation Prompts
Move from a fast scaffold to production details, then tune timing and edge states.
Create a circular progress ring that fills up visually based on a given percentage.
Reference Implementation
The same implementation approach used by the live preview, kept compact enough to inspect and adapt.
"use client";
import { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
export function ProgressRingDemo({ isPlaying = false }: { isPlaying?: boolean }) {
const [progress, setProgress] = useState(0);
useEffect(() => {
if (!isPlaying) {
setProgress(0);
return;
}
const timer = setInterval(() => {
setProgress(p => {
if (p >= 100) return 0;
return p + Math.floor(Math.random() * 15 + 10);
});
}, 350);
return () => clearInterval(timer);
}, [isPlaying]);
const radius = 40;
const circumference = 2 * Math.PI * radius;
const clampedProgress = Math.min(100, Math.max(0, progress));
const strokeDashoffset = circumference - (clampedProgress / 100) * circumference;
return (
<div className="relative flex h-64 w-full items-center justify-center overflow-hidden rounded-2xl bg-white p-6 shadow-sm ring-1 ring-stone-200">
<div className="flex flex-col items-center bg-white p-10 rounded-3xl shadow-sm border border-stone-200">
<div className="relative w-32 h-32 flex items-center justify-center mb-6">
{"text-stone-400 italic">/* Background Ring */}
<svg className="absolute inset-0 w-full h-full -rotate-90">
<circle
cx="64"
cy="64"
r={radius}
stroke="currentColor"
strokeWidth="8"
fill="transparent"
className="text-stone-100"
/>
{"text-stone-400 italic">/* Animated Progress Ring */}
<motion.circle
cx="64"
cy="64"
r={radius}
stroke="currentColor"
strokeWidth="8"
fill="transparent"
strokeLinecap="round"
className="text-blue-500 drop-shadow-sm"
style={{ strokeDasharray: circumference }}
animate={{ strokeDashoffset }}
transition={{ duration: 0.5, ease: "easeInOut" }}
/>
</svg>
<div className="text-xl font-bold text-stone-900 tabular-nums">
{clampedProgress}%
</div>
</div>
<p className="text-stone-500 font-medium text-sm">Uploading file...</p>
</div>
</div>
);
}