Simple svg css progress circle

Viewed 5879

I am trying to look for a way to achieve a simple progress circle (static) with no animations. The examples I have found have very different offsets for percentage such as given in the example below. How do I make my progress circle in such a way that if I provide offset as 50%, then it is exactly 50% (half filled)?

.u-absoluteCenter {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  margin: auto;
}
.u-flexCenter {
  display: flex;
  align-items: center;
  justify-content: center;
}
.u-offscreen {
  position: absolute;
  left: -999em;
}
.demo {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  margin: auto;
  display: flex;
  align-items: center;
  justify-content: center;
}
.progress {
  transform: rotate(-90deg);
}
.progress__value {
  stroke-dasharray: 0;
  stroke-dashoffset: 0;
}
@-webkit-keyframes progress {
  from {
    stroke-dashoffset: 339.292;
  }
  to {
    stroke-dashoffset: 0;
  }
}
@keyframes progress {
  from {
    stroke-dashoffset: 339.292;
  }
  to {
    stroke-dashoffset: 0;
  }
}
<svg width="120" height="120" viewBox="0 0 120 120">
    <circle cx="60" cy="60" r="54" fill="none" stroke="#e6e6e6" stroke-width="12" />
    <circle cx="60" cy="60" r="54" fill="none" stroke="#f77a52" stroke-width="12"
        stroke-dasharray="339.292" stroke-dashoffset="339.292" />
</svg>

2 Answers

You can leverage an SVG attribute to set the path length rather than having to calculate it.

pathLength sets the length to whatever you need...say 100 for a progress bar.

The pathLength attribute lets authors specify a total length for the path, in user units. This value is then used to calibrate the browser's distance calculations with those of the author, by scaling all distance computations using the ratio pathLength/(computed value of path length).

pathLength="100"

Then you can set the stroke-dasharray to 100 as well and then adjust the stroke-dashoffset as needed....

::root {
  --val: 0;
}

svg {
  transform: rotate(-90deg);
}

.percent {
  stroke-dasharray: 100;
  stroke-dashoffset: calc(100 - var(--val));
}

.fifty {
  --val: 50;
}

.sixty {
  --val: 60;
}

.ninety {
  --val: 90;
}
<svg width="120" height="120" viewBox="0 0 120 120">
    <circle cx="60" cy="60" r="54" fill="none" stroke="#e6e6e6" stroke-width="12" />
    <circle class="percent fifty" cx="60" cy="60" r="54" fill="none" stroke="#f77a52" stroke-width="12" pathLength="100" />
</svg>

<svg width="120" height="120" viewBox="0 0 120 120">
    <circle cx="60" cy="60" r="54" fill="none" stroke="#e6e6e6" stroke-width="12" />
    <circle class="percent sixty" cx="60" cy="60" r="54" fill="none" stroke="#f77a52" stroke-width="12" pathLength="100" />
</svg>


<svg width="120" height="120" viewBox="0 0 120 120">
    <circle cx="60" cy="60" r="54" fill="none" stroke="#e6e6e6" stroke-width="12" />
    <circle class="percent ninety" cx="60" cy="60" r="54" fill="none" stroke="#f77a52" stroke-width="12" pathLength="100" />
</svg>

As Paulie says, pathLength is the key to progress circles

A modern Custom Element (supported in all modern browsers) makes for a re-usable HTML Element

 <svg-progress-circle percent="30"></svg-progress-circle>
 <svg-progress-circle percent="20" color="blue"></svg-progress-circle>
 <svg-progress-circle percent="80" color="gold"></svg-progress-circle>

Added a range-input for interactive demo purposes.

Percent is a property on the element, you can set with code like:

    document.getElementById("Slider1").percent = <PERCENTAGE>;

If you don't want a dashed grey fullcircle, delete the dash setting from the pathLenght=120 path

I used a path instead of overlapping circles because with some other settings the almost same code can create pie-charts.

<style>
  svg { width: 150px; background: teal }
  svg-progress-circle[percent="100"] path { stroke: green }
</style>

<svg-progress-circle percent="30"></svg-progress-circle>
<svg-progress-circle percent="20" color="blue"></svg-progress-circle>
<svg-progress-circle percent="80" color="gold"></svg-progress-circle>

<script>
  customElements.define("svg-progress-circle", class extends HTMLElement {
    connectedCallback() {
      let d = 'M5,30a25,25,0,1,1,50,0a25,25,0,1,1,-50,0'; // circle
      this.innerHTML = 
      `<input type="range" min="0" max="100" step="10" value="30"`+ // delete 2 lines
      ` oninput="this.parentNode.percent=this.value" /><br>`+ // just for demo
      
      `<svg viewBox="0 0 60 60">
       <path stroke-dasharray="10 2"   stroke-dashoffset="-19" 
             pathlength="120" d="${d}" fill="grey" stroke="lightgrey" stroke-width="5"/>
       <path stroke-dasharray="30 70" stroke-dashoffset="-25" 
             pathlength="100" d="${d}" fill="none" 
             stroke="${this.getAttribute("color")||"red"}" stroke-width="5"/>
       <text x="50%" y="57%" text-anchor="middle">30%</text></svg>`;
       
      this.style.display='inline-block';
      this.percent = this.getAttribute("percent");
    }
    set percent(val = 0) {
      this.setAttribute("percent", val);
      let dash = val + " " + (100 - val);
      this.querySelector("path+path").setAttribute('stroke-dasharray', dash);
      this.querySelector("text").innerHTML = val + "%";
      this.querySelector("input").value = val;
    }
  })
</script>

Note: I am working on a complete Web Component that does Pie Graphs and fancy Progress circles like:

But, it is one of many side-projects... HTML examples and obfuscated source code available at https://pie-meister.github.io/

Related