Animated Circular Progress Bar using HTML, CSS, & JS

Animated Circular Progress Bar

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.


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:


<!DOCTYPE html>
<html lang="en">
    <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">
    <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. -->
          <div id="number-inside-circle"><p>20%</p></div>
        <button class="button-click" onclick="animateButtonClick()">
          Animate with Random Value

      <script src="./progressbar-animate.js"></script>


@import url(";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;


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(
    .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
      // 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


        // 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);

// Animate with some value when the page loads first time

Leave a Comment