3

I'm trying to make a "progress bar" with animation that changes the background colour of the bar.

The bar should start with red at 0% and as it progresses across the element it changes to green at 100%. I've got this working for 100% (and no, the colour is not great, but that's a future problem)...

setTimeout(function(){
  var bar = document.getElementById("bar");
  bar.style.width = "100%";
  bar.classList.add("show");
},10);
#progress {
  border:1px solid #888;
  background-color:#eee;
  height:
  width:100%;
}
#bar {
  height:30px;
  background-color:red;
  width:0;
  transition: all ease 1s;
}
#bar.show {
  background-color:lightgreen;
}
<div id="progress"><div id="bar"></div></div>

The problem is that (for example) at 50% I cannot get the bar to "stop" at the 50% transition state between the red and the green.

Is there a way to either calculate the colour as it would be at 50% or get CSS to stop the transition at a particular point?

You can see what happens if I have a 50% value... where it still goes all the way to green but stops at the 50% width, but what I want it to do is stop at the 50% transition colour (i.e. going from red to mid-way between red and green)...

setTimeout(function(){
  var bar = document.getElementById("bar");
  bar.style.width = "50%";
  bar.classList.add("show");
},10);
#progress {
  border:1px solid #888;
  background-color:#eee;
  height:
  width:100%;
}
#bar {
  height:30px;
  background-color:red;
  width:0;
  transition: all ease 1s;
}
#bar.show {
  background-color:lightgreen;
}
<div id="progress"><div id="bar"></div></div>

Additional Info (as requested by 0stone0) the percentage value will not be known at the time the page is loaded, but will be provided via an AJAX call.

freefaller
  • 19,368
  • 7
  • 57
  • 87
  • Does this answer your question? [How to get color value from gradient by percentage with javascript?](https://stackoverflow.com/questions/30143082/how-to-get-color-value-from-gradient-by-percentage-with-javascript) – lissettdm Mar 16 '21 at 17:08

3 Answers3

2

'Stopping' a gradient transition could be quite difficult.

Please consider an alternative approach, one where you manually calculate the desired final color. Use this color value as the target for the transition. This way there's no need to 'stop' the transition, since the final colour is already proportional to the percentage.

I've used this function to calculate the gradient between green and red, based on the percentage.

To ensure the bar is always clickable, we've moved the onClick to the .progress div so we can render the .bar at 0 width.

(Updated answer, based on comments)

function load(progress, perc) {
  var bar = progress.getElementsByClassName("bar")[0];
  bar.style.width = perc.toString() + "%";
  bar.style.backgroundColor = getGradient(perc / 100);
}

function getGradient(ratio) {
  var color1 = '90ee90'; // lightgreen
  var color2 = 'FF0000'; // red
  var hex = function(x) {
    x = x.toString(16);
    return (x.length == 1) ? '0' + x : x;
  };

  var r = Math.ceil(parseInt(color1.substring(0,2), 16) * ratio + parseInt(color2.substring(0,2), 16) * (1-ratio));
  var g = Math.ceil(parseInt(color1.substring(2,4), 16) * ratio + parseInt(color2.substring(2,4), 16) * (1-ratio));
  var b = Math.ceil(parseInt(color1.substring(4,6), 16) * ratio + parseInt(color2.substring(4,6), 16) * (1-ratio));
  return '#' + hex(r) + hex(g) + hex(b);
}
.progress {
  border:1px solid #888;
  background-color:#eee;
  height:30px;
  width:100%;
}
.bar {
  height:30px;
  background-color:red;
  width:0;
  transition: all ease 1s;
}
Click to run...<br/>
<div class="progress" onclick='load(this, 25)'><div class="bar">25%</div></div>
<div class="progress" onclick='load(this, 50)'><div class="bar">50%</div></div>
<div class="progress" onclick='load(this, 75)'><div class="bar">75%</div></div>
<div class="progress" onclick='load(this, 100)'><div class="bar">100%</div></div>

(Original answer)

function load(bar, until) {
  var p = 0;
  setInterval(function() {
  
    // Clear on complete
    if (p > until) {
      clearInterval(this);
      return;
    }
    
    // Update Bar
    bar.innerHTML = p;
    bar.style.background = getGradient((p/1)/100);
    bar.style.width = p + "%";
    
    // Bump percentage 
    p++;
  }, 100);
}

function getGradient(ratio) {
    var color1 = '90ee90'; // lightgreen
    var color2 = 'FF0000'; // red
    var hex = function(x) {
        x = x.toString(16);
        return (x.length == 1) ? '0' + x : x;
    };

    var r = Math.ceil(parseInt(color1.substring(0,2), 16) * ratio + parseInt(color2.substring(0,2), 16) * (1-ratio));
    var g = Math.ceil(parseInt(color1.substring(2,4), 16) * ratio + parseInt(color2.substring(2,4), 16) * (1-ratio));
    var b = Math.ceil(parseInt(color1.substring(4,6), 16) * ratio + parseInt(color2.substring(4,6), 16) * (1-ratio));
    return '#' + hex(r) + hex(g) + hex(b);
}
.progress {
  border:1px solid #888;
  background-color:#eee;
  height: 50px;
  width:100%;
}
.bar {
  height:100%;
  width:100%;
}
<div class="progress"><div onClick='load(this, 100)' class="bar"></div></div>
<div class="progress"><div onClick='load(this, 50)'  class="bar"></div></div>
0stone0
  • 34,288
  • 4
  • 39
  • 64
  • I like the idea... but instead of setting the colour directly, it would make more sense to work it out once, and set it as the final colour of the transition. I will have a play – freefaller Mar 16 '21 at 16:11
  • Won't you end up with the same issue? How would you stop suck a transition? Ow, you could calculate the 'duration' based on current and previous percentages, so you wont have to 'stop' the transition. – 0stone0 Mar 16 '21 at 16:13
  • If I can use the function to get the RGB for a particular percentage, then I can set the background-color to be that... then when I add the class the transition will move TO that colour. I'm not that bothered about duration, but if necessary I could also set that in the code – freefaller Mar 16 '21 at 16:17
  • Could you maybe add some more info about the percentage? How is it calculated? API, or just hardcoded? If you know max percentage beforehand, it shouldn;t be that hard. However, when you're not sure how on how much percentage the bar will stop, how would you calcualate the 'final' color? – 0stone0 Mar 16 '21 at 16:20
  • The percentage will be provided via AJAX after the page has loaded – freefaller Mar 16 '21 at 16:21
  • 2
    Ahhh, maybe you could add that to the OP? Opens up a lot more solutions. – 0stone0 Mar 16 '21 at 16:23
  • 1
    @freefaller Won't calling `getGradient(percentage/100)` on your ajax's callback solve your problem ? I mean you only need to know the color value at that percentage right ? Then you might use it as you want ? Because a running transition introduces **time** and can only be handled by `polling` or a nice event driven solution (if there was). – Lakshya Thakur Mar 16 '21 at 16:46
  • Yes, that's exactly what I meant ("If I can use the function to get the RGB for a particular percentage, then I can set the background-color to be that"), and I'm trying it now... keep getting distracted by colleagues! – freefaller Mar 16 '21 at 16:50
  • 1
    Yep, it works without the need for any interval processing.. so the key thing was the `getGradient` code. Based on that I can set the `width` and `background-colour` to the correct colour for the percentage. If you can amend your question (or I can if you're willing to let me) then we're good :-) – freefaller Mar 16 '21 at 17:07
  • Glad you got it working! You've got a working snippet to add to my answer? Otherwise I'll try to create one to improve my answer. But, of course, you can always improve it ;) – 0stone0 Mar 16 '21 at 17:09
  • 1
    I've added my version as a hidden script... please feel free to edit as you wish (minor amendments for the click, as the bar is 0% to start with, so you can't click on it, that's why I moved it to the progress instead) – freefaller Mar 16 '21 at 17:29
  • Hehe, I had the same problem in my snippet, same solution. Ill update my answer in a sec to remove my snippet, and update yours. Thx – 0stone0 Mar 16 '21 at 17:31
  • Excellent - thanks for your time... it's very much appreciated... now I just need to find colours that don't look like slurry when you're at the 33% step ;-) – freefaller Mar 16 '21 at 18:01
1

You can do something like this by getting the computed styles and setting those to your bar element (on click of a button) or when we reach the percentage width of progress element by using polling via setInterval like so :-

var bar = document.getElementById("bar");
    setTimeout(function(){
      bar.style.width = "100%";
      bar.classList.add("show");
    },10);


function stopProgressAt(percentage){
let interval = setInterval(()=>{
 const progress = document.getElementById("progress");
 const width = getComputedStyle(bar).getPropertyValue('width');
if((parseInt(width) * 100)/(Math.floor(progress.offsetWidth)) >= percentage ){
pauseTransition();
setTimeout(()=>clearInterval(interval),0);
}
},0)
}


function pauseTransition(){
     const bgColor = getComputedStyle(bar).getPropertyValue('background-color');
    const width = getComputedStyle(bar).getPropertyValue('width');
     bar.style.width=width;
     bar.style.backgroundColor=bgColor;
    ;}
    
 stopProgressAt(66);   
#progress {
      border:1px solid #888;
      background-color:#eee;
      height:
      width:100%;
    }
    #bar {
      height:30px;
      background-color:red;
      width:0;
      transition: all ease 1s;
    }
    #bar.show {
      background-color:lightgreen;
    }
<div id="progress"><div id="bar"></div></div>
    <button onclick="pauseTransition()">Pause</button>
Lakshya Thakur
  • 8,030
  • 1
  • 12
  • 39
  • Thanks for your answer. Can you show me how that work work with a specific percentage value... say 66%. This is not something that people will manually stop, it's something that will be displayed based on a predetermined percentage value – freefaller Mar 16 '21 at 15:32
  • @freefaller Updated answer by trying a polling mechanism via `setInterval` and `percentage` variable. – Lakshya Thakur Mar 16 '21 at 15:46
  • Thanks for your efforts, but that looks incredibly hacky... it also results (on my Firefox) with the bar reaching the middle (even though I suggested 66% as an example) and then retracting a few pixels – freefaller Mar 16 '21 at 15:48
  • @freefaller You're right, it is hacky. Maybe `animation` keyframes and `pause-state` can be handy for you. But it's working fine for me in **firefox** though. But yes on viewport resize, you won't see the desired behaviour. Would need additional `resize` event handler for the bar to be consistent with the progress width. – Lakshya Thakur Mar 16 '21 at 15:56
  • You can set `percentage` variable to 66 to get what you want in-case you missed it. – Lakshya Thakur Mar 16 '21 at 16:06
  • Ahh I can the depleting bar behaviour. Happens with short transition durations. Alright that's not good. – Lakshya Thakur Mar 16 '21 at 16:12
  • @freefaller Did some changes to not have that depleting bar behaviour. Do you still observe them ? – Lakshya Thakur Mar 16 '21 at 16:25
  • Yes, looks better - but I'm still not happy with it as a solution. I really do appreciate the effort though – freefaller Mar 16 '21 at 16:27
  • @freefaller That's okay. Really doing this for my curiosity. Will see if I can come up with something better. – Lakshya Thakur Mar 16 '21 at 16:28
1

use an opaque overlay, like this:

setTimeout(function() {
  const hue = 120;
  const size = 0.7;
  const H = hue * size;
  const L = (size < .5 ? 1 - size : size) * 50;
  const hsl = `hsl(${H}, 100%, ${L}%)`;
  const progress = document.getElementById('progress');
  progress.style.setProperty('--size', size);
  progress.style.setProperty('--hsl', hsl);
}, 500);
#progress {
  --size: 0;
  --hsl: hsl(0, 100%, 50%);
  position: relative;
  overflow: hidden;
  width: 200px;
  height: 20px;
  border: 1px solid #888;
  background-color: var(--hsl);
  transition: background-color 1s;
}
#bar {
  width: 100%;
  height: 100%;
  background: white;
  transition: margin-left 1s;
  margin-left: calc(var(--size) * 100%);
}
<div id="progress"><div id="bar"></div></div>
n--
  • 3,563
  • 4
  • 9