6

I need to make a circular progress indicator with a color gradient. I also need the 'ends' of the progress circle to be rounded. This image has everything Im trying to achieve:

enter image description here

This code is close but doesnt have the color gradient:

https://codepen.io/adsfdsfhdsafkhdsafjkdhafskjds/pen/OJybqza

var control = document.getElementById('control');
var progressValue = document.querySelector('.progress__value');

var RADIUS = 54;
var CIRCUMFERENCE = 2 * Math.PI * RADIUS;

function progress(value) {
  var progress = value / 100;
  var dashoffset = CIRCUMFERENCE * (1 - progress);

  console.log('progress:', value + '%', '|', 'offset:', dashoffset)

  progressValue.style.strokeDashoffset = dashoffset;
}

control.addEventListener('input', function(event) {
  progress(event.target.valueAsNumber);
});

progressValue.style.strokeDasharray = CIRCUMFERENCE;
progress(60);
.demo {
  flex-direction: column;
  display: flex;
  width: 120px;
}

.progress {
  transform: rotate(-90deg);
}

.progress__meter,
.progress__value {
  fill: none;
}

.progress__meter {
  stroke: grey;
}

.progress__value {
  stroke: blue;
  stroke-linecap: round;
}
<div class="demo">
  <svg class="progress" width="120" height="120" viewBox="0 0 120 120">
        <circle class="progress__meter" cx="60" cy="60" r="54" stroke-width="12" />
        <circle class="progress__value" cx="60" cy="60" r="54" stroke-width="12" stroke="url(#gradient)" />
    </svg>
  <input id="control" type="range" value="60" />
</div>

It looks like this: enter image description here

Ive tried adding a linear-gradient to the stroke but it has no effect:

stroke: linear-gradient(red, yellow);

I also tried stroke="url(#linearColors)", but it also has no affect.

<div class="demo">  
  <svg class="progress" width="120" height="120" viewBox="0 0 120 120">
    <linearGradient id="linearColors" x1="0" y1="0" x2="1" y2="1">
      <stop offset="5%" stop-color="#01E400"></stop>
      <stop offset="25%" stop-color="#FEFF01"></stop>
      <stop offset="40%" stop-color="#FF7E00"></stop>
      <stop offset="60%" stop-color="#FB0300"></stop>
      <stop offset="80%" stop-color="#9B004A"></stop>
      <stop offset="100%" stop-color="#7D0022"></stop>
    </linearGradient>
    <circle class="progress__meter" cx="60" cy="60" r="54" stroke-width="12" />
    <circle class="progress__value" cx="60" cy="60" r="54" stroke-width="12" stroke="url(#linearColors)" />
  </svg>
  <input id="control" type="range" value="60" />
</div>

https://jsfiddle.net/yzqmvd16/

Turnip
  • 35,836
  • 15
  • 89
  • 111
Evanss
  • 23,390
  • 94
  • 282
  • 505
  • 1
    See https://stackoverflow.com/questions/14633363/can-i-apply-a-gradient-along-an-svg-path – Robert Longson Apr 22 '20 at 12:13
  • Maybe because your stroke URL is wrong? The id for the gradient definition is "linearColors" but your url has #gradient. – Michael Mullany Apr 22 '20 at 23:25
  • @MichaelMullany I tried that and updated my question but no change. – Evanss Apr 23 '20 at 07:31
  • SVG does not support "gradients along a path" or "gradients around a circle" (sometimes called conic gradients). If you want that effect, you would need to simulate the gradient using many little elements (as per Robert's link). – Paul LeBeau Apr 23 '20 at 11:16
  • Related (includes an SVG solution) [Ring-shaped process spinner with fading gradient effect around the ring](https://stackoverflow.com/questions/22531861/ring-shaped-process-spinner-with-fading-gradient-effect-around-the-ring) – TylerH Feb 17 '23 at 17:07

3 Answers3

5

Instead of using a gradient you can give the illusion of a gradient by using 100 circles each with a different fill. I'm using the fill-opacity attribute to set the element either fully opaque or fully transparent.

I hope it helps.

const SVG_NS = 'http://www.w3.org/2000/svg';
const CIRCUMFERENCE = base.getTotalLength()
const UNIT = CIRCUMFERENCE / 100;
let circles=[];//the array of circles

//create 100 circles each with a different fill color to create the illusion of a gradient
for(let i = 0; i<100; i++){
  let pos = base.getPointAtLength(i*UNIT);
  let o = {cx:pos.x,cy:pos.y,r:5.5,'fill-opacity':0,fill:`hsl(220,100%,${50 + (100-i)/2}%)`}
  circles.push(drawCircle(o, progress__value));  
}

progress();

control.addEventListener('input', progress);

function progress(){
  let val = control.valueAsNumber;
  for(let i = 0; i<circles.length; i++){
    if(i<=val){
    circles[i].setAttributeNS(null,'fill-opacity',1)    
    }else{
    circles[i].setAttributeNS(null,'fill-opacity',0)
    }
  } 
}

// a function to create a circle
function drawCircle(o, parent) {
  var circle = document.createElementNS(SVG_NS, 'circle');
  for (var name in o) {
    if (o.hasOwnProperty(name)) {
      circle.setAttributeNS(null, name, o[name]);
    }
  }
  parent.appendChild(circle);
  return circle;
}
svg{border:solid}

.demo {
  flex-direction: column;
  display: flex;
  width: 120px;
}

.progress__meter{
    fill: none;
}

.progress__meter {
    stroke: grey;
}
<div class="demo">  
    <svg class="progress"  viewBox="-2 -2 124 124">
        <path class="progress__meter" id="base" d="M60,6A54,54 0 0 1 60,114A54,54 0 0 1 60,6z"  stroke-width="12" />
      <g id="progress__value"></g>
    </svg>
    <input id="control" type="range" value="60" />
</div>
enxaneta
  • 31,608
  • 5
  • 29
  • 42
3

Your original code nearly worked. The problem was that the stroke color of the progress circle was being overridden by the stroke: blue; in the CSS. Removing this allows the gradient to apply to the circle's stroke, as desired.

var control = document.getElementById('control');
var progressValue = document.querySelector('.progress__value');

var RADIUS = 54;
var CIRCUMFERENCE = 2 * Math.PI * RADIUS;

function progress(value) {
  var progress = value / 100;
  var dashoffset = CIRCUMFERENCE * (1 - progress);

  // console.log('progress:', value + '%', '|', 'offset:', dashoffset)

  progressValue.style.strokeDashoffset = dashoffset;
}

control.addEventListener('input', function(event) {
  progress(event.target.valueAsNumber);
});

progressValue.style.strokeDasharray = CIRCUMFERENCE;
progress(60);
.demo {
  flex-direction: column;
  display: flex;
  width: 120px;
}

.progress {
  transform: rotate(-90deg);
}

.progress__meter,
.progress__value {
  fill: none;
}

.progress__meter {
  stroke: grey;
}

.progress__value {
  /* stroke: blue; */
  stroke-linecap: round;
}
<div class="demo">
  <svg class="progress" width="120" height="120" viewBox="0 0 120 120">
    <defs>
      <linearGradient id="linearColors" x1="1" y1="0" x2="0" y2="1">
        <stop offset="5%" stop-color="#01E400"></stop>
        <stop offset="25%" stop-color="#FEFF01"></stop>
        <stop offset="40%" stop-color="#FF7E00"></stop>
        <stop offset="60%" stop-color="#FB0300"></stop>
        <stop offset="80%" stop-color="#9B004A"></stop>
        <stop offset="100%" stop-color="#7D0022"></stop>
      </linearGradient>
    </defs>
    <circle class="progress__meter" cx="60" cy="60" r="54" stroke-width="12" />
    <circle class="progress__value" cx="60" cy="60" r="54" stroke-width="12" stroke="url(#linearColors)" />
  </svg>
  <input id="control" type="range" value="60" />
</div>
Sean
  • 6,873
  • 4
  • 21
  • 46
0

This solution worked best and was provie Ben Ilegbodu https://www.benmvp.com/blog/how-to-create-circle-svg-gradient-loading-spinner/

<svg
  xmlns="http://www.w3.org/2000/svg"
  width="200"
  height="200"
  viewBox="0 0 200 200"
  fill="none"
  color="#3f51b5"
>
  <defs>
    <linearGradient id="spinner-secondHalf">
      <stop offset="0%" stop-opacity="0" stop-color="currentColor" />
      <stop offset="100%" stop-opacity="0.5" stop-color="currentColor" />
    </linearGradient>
    <linearGradient id="spinner-firstHalf">
      <stop offset="0%" stop-opacity="1" stop-color="currentColor" />
      <stop offset="100%" stop-opacity="0.5" stop-color="currentColor" />
    </linearGradient>
  </defs>

  <g stroke-width="8">
    <path stroke="url(#spinner-secondHalf)" d="M 4 100 A 96 96 0 0 1 196 100" />
    <path stroke="url(#spinner-firstHalf)" d="M 196 100 A 96 96 0 0 1 4 100" />

    <!-- 1deg extra path to have the round end cap -->
    <path
      stroke="currentColor"
      stroke-linecap="round"
      d="M 4 100 A 96 96 0 0 1 4 98"
    />
  </g>

  <animateTransform
    from="0 0 0"
    to="360 0 0"
    attributeName="transform"
    type="rotate"
    repeatCount="indefinite"
    dur="1300ms"
  />
</svg>
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Feb 18 '23 at 05:53