CSS percentage of transition (for progress bar)

Viewed 714

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.

3 Answers

'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>

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>

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>

Related