Creating Animated Radial Progress Bars with SVG, CSS and VueJS

Nathan Cockerill
8 min readJul 21, 2018

Radial Progress Bars on the web are a great way to display to the user how far through a process they are.

There are many ways you can make Radial Progress Bars but I find the easiest is to draw them with SVGs and use Vue (or any other JavaScript libraries) to animate them.

First off we need to make the SVG. We can do this easily in Sketch. We would create a circle with a border and no fill. This could be at any size but I am going to make mine 100px by 100px. We would then duplicate that circle so that we have two of them. One of them will be the background of the circle border then the other will be the actual percentage fill of the border.

On the second circle we need to set the border dash to any number. I have chose 50 on this occasion. This defines how much of the circle to fill. We also need to set the gap to 9999 (or any other very high number) so that the dashes of the border will never touch and you will only end up with the one fill path dash.

Then place the second circle on top of the first one to end up with a result looking like the following.

I have added a text layer with ’45%’ as its contents and placed this on top of both circles. All you need to do now is rotate the second circle so that the fill starts from the top of the circle. Take note the amount of degrees you have rotated the fill as we will need this when it comes to coding.

This should be your overall final result with the three layers grouped and the fill path rotated. You can then copy the SVG code for ProgressBar group into CodePen (or any other development environment)

The HTML

<svg width="106px" height="106px" viewBox="0 0 106 106" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 51 (57462) - http://www.bohemiancoding.com/sketch -->
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="ProgressBar" transform="translate(-17.000000, -17.000000)">
<circle id="Oval" stroke="#949494" stroke-width="5" fill-rule="nonzero" cx="70" cy="70" r="50"></circle>
<path d="M70,120 C97.6142375,120 120,97.6142375 120,70 C120,42.3857625 97.6142375,20 70,20 C42.3857625,20 20,42.3857625 20,70 C20,97.6142375 42.3857625,120 70,120 Z" id="Oval-Copy" stroke="#000000" stroke-width="5" stroke-dasharray="50,9999" fill-rule="nonzero" transform="translate(70.000000, 70.000000) rotate(-125.000000) translate(-70.000000, -70.000000) "></path>
<text id="45%" font-family="Avenir-Medium, Avenir" font-size="18" font-weight="400" fill="#000000">
<tspan x="52" y="76">45%</tspan>
</text>
</g>
</g>
</svg>

We can remove the following three lines of code as they don’t serve any purpose to what we are doing.

<!-- Generator: Sketch 51 (57462) - http://www.bohemiancoding.com/sketch -->
<desc>Created with Sketch.</desc>
<defs></defs>

You can also remove the text tag from the SVG as we will code this dynamically.

<text id="45%" font-family="Avenir-Medium, Avenir" font-size="18" font-weight="400" fill="#000000">
<tspan x="52" y="76">45%</tspan>
</text>

We can remove the width and the height attribute from the SVG tag as we can control this with css. We also want to add a class to the SVG tag so we can style this.

// Opening SVG tag Exported from Sketch
<svg width="106px" height="106px" viewBox="0 0 106 106" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
// Revised Opening SVG tag
<svg class="progress-circle" viewBox="0 0 106 106" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">

The next thing we can do is add a class onto the path with the stroke-dasharray attribute. I have called this class progress-circle__path. Also on this span tag, we want to change the stroke-dasharray to be a computed Vue property. This is needed in order for us to calculate what value it should be based on the percentage the progress circle is at. We can do this by adding a colon before the attribute and changing the attributes content to be a variable name instead. The finished path should look something like this:

<path class="progress-circle__path" :stroke-dasharray="circle" d="M70,120 C97.6142375,120 120,97.6142375 120,70 C120,42.3857625 97.6142375,20 70,20 C42.3857625,20 20,42.3857625 20,70 C20,97.6142375 42.3857625,120 70,120 Z" id="Oval-Copy" stroke="#000000" stroke-width="5" fill-rule="nonzero" transform="translate(70.000000, 70.000000) rotate(-125.000000) translate(-70.000000, -70.000000) "></path>

We want to add a span element which will contain the percentage as a string. We can add a class to this span and place it above the starting SVG tag. The contents of this span would be something like {{ percent }}% and percent would be a data value referenced from Vue.

<span class="progress-circle__percent">{{ percent }}%</span>

Then in order to get the percentage to show in the centre of the progress circle we want to wrap the percent span and the SVG tag within a containing div. I will call this progress-circle__container.

We want to add a button after the progress-circle__container which will generate a new percentage number each time it is pressed via a Vue method.

<a href=”#” @click=”randomNumber”>Generate New Percentage</a>

The final thing we need to do in the HTML is to wrap the progress-circle__container and the generate new percentage button in a containing div so that we can initiate a Vue instance.

The final HTML/Vue template should look like this:

<div id="app">

<div class="progress-circle__container">

<span class="progress-circle__percent">{{ percent }}%</span>
<svg class="progress-circle" viewBox="0 0 106 106" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="ProgressBar" transform="translate(-17.000000, -17.000000)">
<circle id="Oval" stroke="#949494" stroke-width="5" fill-rule="nonzero" cx="70" cy="70" r="50"></circle>
<path class="progress-circle__path" :stroke-dasharray="circle" d="M70,120 C97.6142375,120 120,97.6142375 120,70 C120,42.3857625 97.6142375,20 70,20 C42.3857625,20 20,42.3857625 20,70 C20,97.6142375 42.3857625,120 70,120 Z" id="Oval-Copy" stroke="#000000" stroke-width="5" fill-rule="nonzero" transform="translate(70.000000, 70.000000) rotate(-125.000000) translate(-70.000000, -70.000000) "></path>
</g>
</g>
</svg> </span> </div> <a href="#" @click="randomNumber">Generate New Percentage</a></div>

The CSS

I am using the SCSS preprocessor in CodePen. This will compile to CSS.

As we removed the width and the height attributes from the SVG tag. We need to add a max-width and a max height property onto the SVG and set these to be the ones you used in Sketch. Our circle was 100px by 100px. A width of 100% is also added to the SVG. The next line of code we want to add is to correctly position the top fill path layer. In Sketch, I rotated the fill path by -125 degrees. We want to get to -180 degrees so we would add a rotation of -55 degrees in the CSS. We would also want to turn the negative 180 into a positive 180 (flip the fill path horizontally) so we can also add a scaleX property of -1.

.progress-circle {
max-width:100px;
max-height:100px;
width:100%;
transform: scaleX(-1) rotate(-55deg);
}

The next thing we want to do is to centre the text to the middle of the progress bar. We named the text .progress-circle__percent so we can add &__percent { } onto the .progress-circle class selector.

To centre the text we can use the following 4 lines of code.

.progress-circle {  &__percent { 
position:absolute;
top:50%;
left:50%;
transform: translate(-50%,-50%);
}
}

We wrapped a .progress-circle__container around the span tag and the SVG tag. We want to display:inline-block this and add a position:relative so that the container is only as wide as it needs to be and so that everything that is positioned absolutely within it (the text) stays within that div.

.progress-circle {  &__container {
display:inline-block;
position:relative;
}
}

The final line of style we want to add is to the fill path. When we change the percentage number to different each time, we want a nice transition between the two numbers. We don’t want it to just jump from one percentage to another. We can add a css transition property to do this.

.progress-circle {  &__path {
transition: 0.5s ease-in-out all;
}
}

The VueJS

Now that all of the styles which we need have been added, we just need to add the functionality behind it.

We need to create a fresh Vue instance. We will hook this up to the #app div.

var app = new Vue({
el: '#app',
data: {

},
});

We want to set a default percentage to show in our progress circle. Ours is going to be 50% so we can add a property called percent into our data array.

var app = new Vue({
el: '#app',
data: {
percent: 50
},
});

We added a @click=”randomNumber” event onto the anchor tag in the html so we need to a method in Vue which corresponds to this. We need to write a method that sets our percent data property to a random number between 1 and 100. We can add this method to the methods array.

var app = new Vue({
el: '#app',
data: {
percent: 50
},
methods: {
randomNumber() {
this.percent = Math.floor(Math.random() * (100 - 1 + 1)) + 1;
}
},
});

The only thing left to do is to calculate how much of the circle the fill path needs to cover. We would do this by creating a computed property within Vue. We called this circle within the html. This property will sit within a computed array on our Vue instance.

The equation to get the circumference of a circle is πd (Pi multiplied by the diameter). We would then times the circumference by the percentage (chosen at random using the randomNumber method). The diameter of our circle is 100px so the computed property code for circle would be:

computed: {
circle() {
return ((this.percent / 100) * 100 * Math.PI) + ',9999';
}
}

The overall Vue instance would now look like this:

var app = new Vue({
el: '#app',
data: {
percent: 50
},
methods: {
randomNumber() {
this.percent = Math.floor(Math.random() * (100 - 1 + 1)) + 1;
}
},
computed: {
circle() {
return ((this.percent / 100) * 100 * Math.PI) + ',9999';
}
}
});

We can test our JavaScript by clicking on the Generate New Percentage button.

HOORAY, IT WORKS!!

CodePen Demo of this example: https://codepen.io/nathancockerill/full/QBGjPr/

--

--