What You'll Learn
Progress rings--those circular indicators that show completion percentage or skill levels--appear throughout modern web interfaces. From file upload animations to skill dashboards, these visual components communicate status and progress more intuitively than linear bars.
This guide walks you through creating SVG-based progress rings quickly, whether you're building a Vue component, React application, or need a pure CSS solution. The technique relies on two SVG circle elements and a clever use of dash patterns to create the fill effect.
For building other interactive UI components, explore our guides on building inclusive toggle buttons and CSS blend modes for advanced visual effects.
SVG offers several advantages that make it the preferred choice
Perfect Scalability
SVGs scale perfectly without pixelation, looking crisp on retina displays and when zoomed.
CSS Styling
Direct access to stroke properties through CSS enables smooth animations and easy customization.
DOM Integration
SVG is part of the DOM, so you can manipulate it with JavaScript for dynamic updates.
Lightweight
Minimal file size with instant loading, unlike GIF animations or video files.
The Core SVG Technique
Understanding Circle Properties
Every SVG progress ring starts with a basic circle element. The key properties are:
- cx, cy: Center coordinates of the circle
- r: Radius determining the circle's size
- stroke-width: Thickness of the circle's outline
<svg width="200" height="200" viewBox="0 0 200 200">
<circle cx="100" cy="100" r="90" stroke-width="20" />
</svg>
The circumference--the distance around the circle's edge--follows the formula: circumference = 2 × π × r. For a radius of 90, this equals approximately 565.5 units.
The Dash Pattern Magic
The progress effect relies on two SVG properties:
- stroke-dasharray: Creates a dashed stroke pattern
- stroke-dashoffset: Controls where the dash pattern begins
When stroke-dasharray equals the circumference and stroke-dashoffset is 0, the entire circle is visible. Increasing the offset hides portions of the circle, revealing exactly the desired percentage.
Calculating Progress Values
const circumference = 2 * Math.PI * r;
const offset = circumference * (1 - progress / 100);
For 75% progress with r=90: offset = 565.5 × (1 - 0.75) = 141.4
Pure CSS Implementation
For simple use cases, a pure CSS approach provides a lightweight solution:
<div class="progress-ring">
<svg viewBox="0 0 200 200">
<circle class="progress-ring__circle" cx="100" cy="100" r="90" />
</svg>
<span class="progress-ring__label">75%</span>
</div>
Key CSS Properties
.progress-ring svg {
transform: rotate(-90deg);
}
.progress-ring__circle--progress {
stroke: #4caf50;
stroke-dasharray: 565.5;
stroke-dashoffset: 141.4;
transition: stroke-dashoffset 0.5s ease;
}
The transform: rotate(-90deg) ensures progress begins at the top. The transition property enables smooth animations when progress values change.
CSS Custom Properties
For dynamic updates, use CSS variables:
<svg style="--progress: 65; --circumference: 565.5;">
<circle stroke-dashoffset="calc(var(--circumference) * (1 - var(--progress) / 100))" />
</svg>
Progress rings are just one example of fluid type and space scales that create cohesive, responsive designs across all screen sizes.
Vue Component Implementation
A reusable Vue 3 component with TypeScript:
<script setup lang="ts">
const props = defineProps<{
size?: number;
strokeWidth?: number;
trailColor?: string;
strokeColor?: string;
progress: number | string;
}>();
const { size = 150, strokeWidth = 20 } = props;
const cy = size / 2;
const r = cy - strokeWidth / 2;
const circumference = 2 * Math.PI * r;
const dashOffset = computed(() =>
circumference * (1 - Number(props.progress) / 100)
);
</script>
<template>
<svg :width="size" :height="size">
<circle :cx="cy" :cy="cy" :r="r" :stroke="trailColor" fill="none" :stroke-width="strokeWidth" />
<circle :cx="cy" :cy="cy" :r="r" fill="none" :stroke="strokeColor"
:transform="`rotate(-90 ${cy} ${cy})`"
:stroke-dasharray="circumference" :stroke-dashoffset="dashOffset" />
</svg>
</template>
Using foreignObject
Embed HTML content inside SVG using foreignObject:
<foreignObject :x="0" :y="0" :width="size" :height="size">
<div class="flex items-center justify-center w-full h-full">
<slot>{{ progress }}%</slot>
</div>
</foreignObject>
This allows placing labels, icons, or buttons directly within the ring.
React Implementation with Hooks
React component with animated progress updates:
const ProgressRing = ({
progress = 0, size = 150, strokeWidth = 20,
trailColor = '#e0e0e0', strokeColor = '#4caf50'
}) => {
const center = size / 2;
const radius = center - strokeWidth / 2;
const circumference = 2 * Math.PI * radius;
const offset = circumference - (progress / 100) * circumference;
return (
<svg width={size} height={size} style={{ transform: 'rotate(-90deg)' }}>
<circle cx={center} cy={center} r={radius} fill="none"
stroke={trailColor} strokeWidth={strokeWidth} />
<circle cx={center} cy={center} r={radius} fill="none"
stroke={strokeColor} strokeWidth={strokeWidth}
strokeDasharray={circumference} strokeDashoffset={offset}
strokeLinecap="round"
style={{ transition: 'stroke-dashoffset 0.5s ease' }} />
</svg>
);
};
Animated Progress Updates
const AnimatedProgressRing = ({ targetProgress }) => {
const [currentProgress, setCurrentProgress] = useState(0);
useEffect(() => {
const duration = 500;
const start = currentProgress;
const change = targetProgress - start;
const startTime = performance.now();
const animate = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const easeOut = 1 - Math.pow(1 - progress, 3);
setCurrentProgress(start + change * easeOut);
if (progress < 1) requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
}, [targetProgress]);
return <ProgressRing progress={currentProgress} />;
};
Accessibility Considerations
Screen Reader Support
Add ARIA attributes for assistive technologies:
<div role="progressbar"
aria-valuenow="75"
aria-valuemin="0"
aria-valuemax="100"
aria-label="File upload progress">
<svg aria-hidden="true">...</svg>
<span class="sr-only">75% complete</span>
</div>
role="progressbar"identifies the element as a progress indicatoraria-valuenow,aria-valuemin,aria-valuemaxprovide the numeric values- Visually hidden text ensures screen readers announce the progress
Reduced Motion Preferences
Respect user preferences for reduced animation:
@media (prefers-reduced-motion: reduce) {
.progress-ring circle {
transition: none;
}
}
Building accessible UI components like progress rings aligns with our commitment to inclusive design practices.
Dashboard Skill Indicators
Display proficiency levels for technologies on developer portfolios. The circular format fits naturally into grid layouts.
File Upload Progress
Show upload status in constrained areas. Works well with attachment icons or compact upload buttons.
Goal Tracking
Health and productivity apps use rings for daily goals--steps, water, habits completed. Visual metaphor aligns with time tracking.
Quiz Progress
Educational platforms display course completion. Animating as users progress creates satisfying feedback.
Multi-Segment Rings
Display multiple related metrics with different colored arcs. Provides rich data visualization in a compact format.
Interactive Elements
Transform rings into interactive components with hover effects. Click to expand and show detailed information.
Styling Variations
Gradient Progress Rings
.progress-ring__circle--gradient {
stroke: url(#progressGradient);
}
<svg>
<defs>
<linearGradient id="progressGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#4facfe" />
<stop offset="100%" stop-color="#00f2fe" />
</linearGradient>
</defs>
</svg>
Performance Tips
- Keep SVG simple with minimum necessary elements
- Avoid complex filters for critical paths
- Use SVG for most cases; consider canvas for real-time data
- CSS
conic-gradientalternatives exist but have limited animation support
Browser Compatibility
SVG stroke-dasharray and stroke-dashoffset work in all modern browsers including IE9+. CSS custom properties and transforms have broad support, making SVG progress rings safe for production.