Pure* CSS circular progress bar

Posted 12/6/2022

A pure* CSS, configurable circular progress bar. Yep.

Using only variables, clipping, and transformations… *and a little helper class. ;)

There are several examples and implementations out there. I merged and optimized. And added simple variables so you can reuse and configure the indicator to your likings.

The HTML is minimal

<div class="progress" id="the-progress-bar">
  <div class="circle"></div>
  <div class="circle right"></div>

Update the progress bar: 1 find it, 2 set a helper for when you cross over 50%, 3 convert the progress (a value between 0 and 1 where 1 means 100%) to degrees, and 4 set this value to the --progress property.

function setProgress(progress, id) {
  const bar = document.getElementById(id);
  bar.classList.toggle('beyondHalf', progress > 0.5)
  progress = `rotate(${360 * progress}deg)`;
  bar.style.setProperty('--progress', progress);

setProgress(0.75, "the-progress-bar")

The gist of how the CSS works: using the two .circle boxes, both with rounded corners so that they become circles. Applying a border to them and using clipping to just show a part of it, which is visualizing the progress by rotating the boxes. Once progress passes 50%, a helper class .beyondHalf is used to remove the clipping, showing the box on the right with a full 180° circle and letting the box on the left continue showing the progress.

.progress {
  --color: #f80;
  --diameter: 80px;
  --thickness: 4px;
  --progress: rotate(0deg);
  width: var(--diameter);
  height: var(--diameter);
  position: absolute;
  clip: rect(0px, var(--diameter), var(--diameter), calc(var(--diameter) / 2));

.circle {
  width: calc(var(--diameter) - (2 * var(--thickness)));
  height: calc(var(--diameter) - (2 * var(--thickness)));
  border: var(--thickness) solid var(--color);
  border-radius: calc(var(--diameter) / 2);
  position: absolute;
  clip: rect(0px, calc(var(--diameter) / 2), var(--diameter), 0px);
  will-change: transform;
  transition: transform 300ms;
  transform: var(--progress);

.beyondHalf {
  clip: rect(auto, auto, auto, auto);

.beyondHalf .circle.right {
  transform: rotate(180deg);

See it live in action with all the CSS, JavaScript, and HTML in this demo here.