After looking around on the web, it seems that the most common way to create the radial progress bar is to make use of SVG’s circle
element and its stroke-dasharray
property to achieve the progress. So let’s do that.
Defining the SVG canvas
We set up a 100 by 100 unit viewBox size for easier calculation, therefore the center will be at (50,50)
. There will be two circles: one for the trail and the other for the actual progress.
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="50" fill="none" />
<circle cx="50" cy="50" r="50" fill="none" />
</svg>
Calculating the progress
The most important thing about the component is to determine how far the stroke should be around the circle. In simple terms, the percentage will be a fraction to the circumference of the circle. Therefore the following equation will determine how long the stroke will be in the stroke-dasharray
property.
percent * circumference = strokeLength
And to calculate the circumference, its elementary math:
2 * PI * radius = circumference
By default, the stroke will be drawn along the circle path. However, we want it to draw inside the circle, so we have to do some calculation for actual radius for the circle. Here is the math:
radius = 50 - (strokeWidth / 2)
We use 50 units as it is half of the viewBox size to refer to the original radius. Then we minus by half of the strokeWidth to get the actual radius to be drawn.
Computed properties
So here is the code for the computed properties to be bounded to the template based on the formulaes we went through earlier.
const radius = computed(() => {
return 50 - props.strokeWidth / 2;
});
const circumference = computed(() => 2 * Math.PI * radius.value);
const strokeDasharray = computed(() => {
return `${(props.percent / 100) * circumference.value} ${
circumference.value
}`;
});
The strokeDasharray
value has two values separated by a space. The first value is the length of the stroke and the second value is the length of the gap. So for the gap, we just set a high enough number so as not to repeat the dash array.
RadialProgress component
So here is the code for the component: with the ability to set some custom props:
strokeWidth
percent
size
trailColor
strokeColor
strokeLinecap
<script setup lang="ts">
import { computed } from "vue";
const props = withDefaults(
defineProps<{
strokeWidth?: number;
percent?: number;
size?: string;
trailColor?: string;
strokeColor?: string;
strokeLinecap?: "round" | "square" | "butt";
}>(),
{
strokeWidth: 10,
percent: 0,
size: "64px",
trailColor: "#eee",
strokeColor: "currentColor",
strokeLinecap: "round",
}
);
const radius = computed(() => {
return 50 - props.strokeWidth / 2;
});
const circumference = computed(() => 2 * Math.PI * radius.value);
const strokeDasharray = computed(() => {
return `${(props.percent / 100) * circumference.value} ${
circumference.value
}`;
});
</script>
<template>
<svg
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
:width="size"
:height="size"
>
<circle
cx="50"
cy="50"
:r="radius"
fill="none"
:stroke-width="strokeWidth"
:stroke="trailColor"
/>
<circle
cx="50"
cy="50"
:r="radius"
fill="none"
:stroke-dasharray="strokeDasharray"
:stroke-width="strokeWidth"
:stroke="strokeColor"
:stroke-linecap="strokeLinecap"
transform="rotate(-90, 50, 50)"
/>
</svg>
</template>
One thing to note here is that because by default the stroke starts at the “3 o’clock” position, but we want it to start at the “12 o’clock” position, we rotate the progress circle by
-90
degrees.
Demo
Drag the range slider to see the radial progress in action.
Conclusion
Using SVG Circle element with stroke-dasharray
property really makes it easy to achieve the radial progress component. Coupled with some simple math, we really make the radial progress component easy to use and implement.