Building Progress Ring Quickly

Learn the SVG techniques behind circular progress indicators and build reusable components with Vue, React, or pure CSS.

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.

Why SVG for Progress Rings?

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 indicator
  • aria-valuenow, aria-valuemin, aria-valuemax provide 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-gradient alternatives 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.

Frequently Asked Questions

Build Custom Progress Indicators for Your Project

Our web development team creates polished, accessible UI components including animated progress rings, dashboards, and interactive data visualizations.