Circular Progress Bar with Animation | HTML, CSS, and JavaScript

Circular Progress Bar HTML CSS JS

This is a CSS circular progress bar with animation. It uses JavaScript’s animate() function to animate the progress bar. You can download the source code for free below.

The foreground circle is drawn using SVG. The SVG’s width equals to foreground circle’s width (which is 180px). The cx, cy, and r values should be half of the SVG’s width.

The CSS progress bar contains two circles for animation. They are foreground and background, The foreground circle gets animated when you click on the button. There is also a number inside the circle. It is animated using the setInterval() method in JS.

When you click on the animate button, a random number will be generated and passed to the animteCircle() function. The function calculates the strokeDashoffset for the SVG. We need two values – initial and final. The initial value is the same as the previous value. The final value is the offsetValue.

The number inside the circle gets animated from its previous value. It uses the setInterval() method. When the animation is completed, clearInterval() is called with the corresponding intervalId. The animation speed of the number depends on the circle.

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:

circular-progress-bar.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>
      Circular Progress Bar using HTML, CSS & JS | SemicolonSpace.com
    </title>
    <link rel="stylesheet" href="./circular-progress-bar-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 (180px).
            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"
              stroke-linecap="round"
            />
          </svg>
        </div>
        <div class="text-inside-circle">
          <p id="number-inside-circle">65 GB</p>
          <p class="remaining-text">Remaining</p>
        </div>
      </div>
      <button class="button-click" onclick="animateButtonClick()">
        Animate with Random Value
      </button>
    </div>
    <script src="./circular-progress-bar.js"></script>
  </body>
</html>

circular-progress-bar-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 = container's size (200px) - ( 2 * svg's stroke-width (24px) ) .
        Increase a little bit */
  width: 156px;
  height: 156px;
  border-radius: 50%;
  box-shadow: 1px 1px 10px 2px rgba(0, 0, 0, 0.2);

  /* To center it inside the container.
        margin = ( container's size (200px) - the element's size (156px) ) / 2
        */
  margin: 22px;
  position: absolute;
}

.foreground-circle {
  width: 180px;
  height: 180px;
  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: 10px;
}

.foreground-circle svg circle {
  /* stroke-dasharray = 2 * (22/7) * svg's radius value (90px)*/
  stroke-dasharray: 566;
}

.text-inside-circle {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  font-family: "Roboto", sans-serif;
}

#number-inside-circle {
  font-size: 1.8rem;
  font-weight: 700;
}

.remaining-text {
  font-weight: 400;
  font-size: 0.7rem;
}

.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;
}

circular-progress-bar.js:

var maxValue = 100;

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 maxValue
function animteCircle(value) {
  var offsetValue = Math.floor(
    ((maxValue - value) * svgStrokeDashArray) / maxValue
  );

  // 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 + " GB";
      }
    }, speed);
  }
}

function animateButtonClick() {
  var randomValue = Math.floor(Math.random() * (maxValue + 1));
  console.log("Random Value: ", randomValue);
  animteCircle(randomValue);
}

// Animate with some value when the page loads first time
animteCircle(65);

Leave a Comment