
This is an animated circular progress bar made with HTML, CSS, and JavaScript (JS). It shows the percentage at the center of the circle.
The foreground circle is made with SVG. It’s width equals to foreground-circle’s width – background-circle’s border width. The cx, cy, and r values should be half of the svg’s width. You can change the colors and size as per your requirements.
The CSS animated progress bar draws the circle using SVG. The background circle stays the same. It is made with a div and border-radius 50%. I set the border width to 10px. I set the wrapper’s display property to flex to position it at the center.
For animation purpose, it uses JavaScript’s animate() method. When you click on the animate button, a random number will be generated and passed to the animteCircle() method. The method calculates the offsetValue for the SVG. The final value of the animation equals this offsetValue and the initial value equals the previous strokeDashoffset value. To animate the text, it uses setInterval() method. To stop the animation at the end, clearInterval() method is called with the intervalId.
Related:
Final output:
If the video isn’t working, watch it on the YouTube.
Note: setInterval() method is lagging in Firefox.
Here is the Source Code:
progressbar.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Animated Circular Progress Bar using HTML, CSS, JS</title>
<link rel="stylesheet" href="./progressbar-styles.css">
</head>
<body>
<div class="wrapper">
<div class="container">
<div class="background-circle"></div>
<div class="foreground-circle">
<!-- svg's width (180px) =
foreground-circle's width (190px) - background-circle's border width (10px).
cx, cy, and r values should be half of the svg's width. -->
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="180px"
height="180px"
>
<circle
cx="90"
cy="90"
r="90"
stroke="#50c878"
stroke-width="24"
fill="transparent"
/>
</svg>
</div>
<div id="number-inside-circle"><p>20%</p></div>
</div>
<button class="button-click" onclick="animateButtonClick()">
Animate with Random Value
</button>
</div>
<script src="./progressbar-animate.js"></script>
</body>
</html>
progressbar-styles.css:
@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.wrapper {
width: 300px;
height: 300px;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.container {
width: 200px;
height: 200px;
}
.background-circle {
width: 190px;
height: 190px;
border: 10px solid rgba(80, 200, 120, 0.4);
border-radius: 50%;
/* To center it inside the container */
margin: 5px;
position: absolute;
}
.foreground-circle {
width: 190px;
height: 190px;
padding: 10px;
display: flex;
justify-content: center;
align-items: center;
/* To start the svg circle from the top */
transform: rotate(-90deg);
/* To center it inside the container */
position: absolute;
margin: 5px;
}
.foreground-circle svg circle {
/* stroke-dasharray = 2 * (22/7) * svg's radius value (90px)*/
stroke-dasharray: 566;
}
#number-inside-circle {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
font-family: "Roboto", sans-serif;
font-size: 1.3rem;
font-weight: 700;
}
.button-click {
background-color: #50c878;
border: 1px solid transparent;
padding: 0.4em 0.8em;
border-radius: 0.4em;
color: whitesmoke;
cursor: pointer;
margin-top: 10px;
font-family: "Roboto", sans-serif;
font-size: 1rem;
font-weight: 400;
}
/* To prevent svg border being cut off */
svg:not(:root) {
overflow: visible;
}
progressbar-animate.js:
var svgCircle = document.querySelectorAll(".foreground-circle svg circle");
var numberInsideCircle = document.getElementById("number-inside-circle");
// Get the stroke-dasharray value from CSS
var svgStrokeDashArray = parseInt(
window
.getComputedStyle(svgCircle[0])
.getPropertyValue("stroke-dasharray")
.replace("px", "")
);
// To animte the circle from the previous value
var previousStrokeDashOffset = svgStrokeDashArray;
// To animate the number from the previous value
var previousValue = 0;
var animationDuration = 1000;
// Call this method and pass any value to start the animation
// The 'value' should be in between 0 to 100
function animteCircle(value) {
var offsetValue = Math.floor(((100 - value) * svgStrokeDashArray) / 100);
// This is to animate the circle
svgCircle[0].animate(
[
// initial value
{
strokeDashoffset: previousStrokeDashOffset,
},
// final value
{
strokeDashoffset: offsetValue,
},
],
{
duration: animationDuration,
}
);
// Without this, circle gets filled 100% after the animation
svgCircle[0].style.strokeDashoffset = offsetValue;
// This is to animate the number.
// If the current value and previous values are same,
// no need to do anything. Check the condition.
if (value != previousValue) {
var speed;
if (value > previousValue) {
speed = animationDuration / (value - previousValue);
} else {
speed = animationDuration / (previousValue - value);
}
// start the animation from the previous value
var counter = previousValue;
var intervalId = setInterval(() => {
if (counter == value || counter == -1) {
// End of the animation
clearInterval(intervalId);
// Save the current values
previousStrokeDashOffset = offsetValue;
previousValue = value;
} else {
if (value > previousValue) {
counter += 1;
} else {
counter -= 1;
}
numberInsideCircle.innerHTML = counter + "%";
}
}, speed);
}
}
function animateButtonClick() {
var randomValue = Math.floor(Math.random() * 101);
console.log("Random Value: ", randomValue);
animteCircle(randomValue);
}
// Animate with some value when the page loads first time
animteCircle(65);