Search code examples
javascriptcssanimationprogress-barwidth

How to achieve smooth CSS animation on CSS width change?


I'm working on my own progress bar which will be filling up over different periods of time from 5 minutes to 1 second. My idea for this is:

  1. I have a bar that has CSS width set to 0%, every x seconds
  2. Then I increase the bar width by 1% using JS.

It works like this: https://jsfiddle.net/dnh4y39c/

There are a few issues with this approach:

  1. the animation is smooth only for <2 seconds (100 intervals of 20ms), anything over that and it's visibly not smooth enough, see: https://jsfiddle.net/dnh4y39c/1/ ,

  2. I can battle the issue with CSS transition but it totally breaks timing, if I set transition: all 0.5s; on the bar then it works pretty much well for longer times but it's totally broken on faster loads, eg. progress bar should be done in 1s but instead it stops for 0.5s and then animates 100% all at once, see: https://jsfiddle.net/dnh4y39c/1/

I'm wondering if I should fiddle with width or maybe just move to canvas? I don't know canvas that good but I guess it might be more performant and take up less resources? I might have multiple progress bars within my app as I'm required to load all modules separately (so no one simple loader but might be even over 50 in some places - I know, it's crazy but I'm just following the docs).

Thanks for any input :)


Solution

  • A couple of things to consider would be:

    • avoid using setInterval(), which can produce "jumpy" animation behavior if the interval callback takes longer to execute that the duration of the interval of itself. Instead use setTimeout() if inter-frame delays are needed
    • consider using window.requestAnimationFrame() to update the progress bar's animation. Using window.requestAnimationFrame() ensures that your progress bar's update is in sync with the browsers repaint/redraw cycle

    An updated version of your code, that factors these two ideas in could look like this:

    function interation(element, iterationDuration, i) {
    
      window.requestAnimationFrame(function() {
    
        /*
        Update the width and transition duration of the element for this iteration
        */
        element.style.width = `${i}%`;
        element.style.transitionDuration = `${iterationDuration}ms`;
    
        /*
        Increment/calculate next percentage value for progress bar
        */
        const next = (i === undefined ? 0 : i) + 1;
        if (next <= 100) {
    
          /* 
          Pass element, timeout, and next value through as arguments
          to next interation() call
          */
          setTimeout(interation, iterationDuration, element, iterationDuration, next);
        }
      })
    }
    
    interation(document.querySelector(".progress__bar"), 20);
    // interation(document.querySelector(".progress__bar"), 2000); Long interval
    // interation(document.querySelector(".progress__bar"), 20); Short interval
    * {
      box-sizing: border-box;
    }
    
    .progress__bar {
      transition: width linear;
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      border-radius: 4px;
      background: #f00;
    }
    <div class="item">
      <div class="item__progress">
        <div class="progress__bar" style="width: 0%;"></div>
      </div>
    </div>