What are progress bars?

Believe it or not, we are all familiar with progress bar from our experiences of working with a computer machine to carry out a given task, most commonly downloading software.

For instance, when we download a particular file from a browser on Windows, the button of the browser window in the taskbar starts filling up with a green-colored bar to indicate the portion of the file downloaded successfully.

Even software installers and antivirus programs display these long rectangle bars filling up as more and more stuff is processed.

In simple words:

A progress bar is a means of showing the progress made in a particular computational task.

The idea of tracking the progress of tasks is very useful in situations such as downloads, uploads and installations. However, it can even fit well within our very own slider, and tell us exactly when it is about to be navigated automatically.

Now before we begin, one very important thing to keep in mind is that a progress bar in a slider may not be the most desirable feature out there that sets the slider apart. In fact, most of the sliders out there don't use it at all.

So then why are we wasting time in learning it?

Well, by learning how to create a progress bar in our slider, we'll in turn get to know about a great deal of ideas in CSS and JavaScript.

Precisely speaking, we'll get to know about CSS transitions, their limitations when ought to be controlled by JavaScript, asynchronous vs. synchronous execution of code, browser reflow and repainting, and much more.

These ideas will altogether help us refine our skills in CSS and JavaScript to a great extent and in the end create programs that utilize them in the most efficient way.

Basic HTML markup

First thing's first, let's come up with the HTML for a progress bar.

Here's a simple example created using very elementary HTML:

Can you write the HTML to represent this very progress bar?

Think about it carefully. As before when we were constructing the markup for the pagination feature in the Slider Pagination chapter, you need to ask yourself such simple questions as:

  1. What elements would I need in the progress bar?
  2. What class names would I give to them, given that this progress bar will ultimately go in the slider?

Assuming that you've given it a go, let's now write the markup together.

To begin with, we'll need a basic <div> container for the whole progress bar. It'll be the light grey rectangle shown in the example above. And since it'll soon be part of our slider, we'll call it .slider_progress-bar, following the BEM naming convention we've been using uptil now.

Secondly, as we can notice in the same example above, there is a moving yellow bar inside the grey rectangle. We'll represent this using another <div> and call it .slider_progress-bar_bar.

This gives us the following HTML for the progress bar:

<div class="slider_progress-bar">
   <div class="slider_progress-bar_bar"></div>
</div>

Within our slider's markup, this piece of HTML will go at...

Well, what do you think?

It'll go inside .slider directly, either before the .slider_slides-cont element or after it. We'll go with the former here since that gives a more harmonious look to the slider with the progress bar at the top and the pagination at the bottom.

Here's the complete markup of the progress bar as included in the slider:

<div class="slider">
<div class="slider_progress-bar">
<div class="slider_progress-bar_bar"></div>
</div> <div class="slider_slides-cont"> <div class="slider_slide"><img src="pexels-stijn-dijkstra-2499786.jpg"></div> <div class="slider_slide"><img src="pexels-addie-3152128.jpg"></div> <div class="slider_slide"><img src="pexels-nextvoyage-3520548.jpg"></div> </div> <div class="slider_pagination"></div> <button class="slider_nav">&larr; Previous</button> <button class="slider_nav">Next &rarr;</button> </div>

With the markup formulated, let's now move on to writing some CSS to make it resemble the example shown above.

CSS — a touch of style

The first thing that we ought to settle on is what width to apply to the whole progress bar in our slider. Fortunately, the matter is quite straightforward.

We'll go with assigning the progress bar the same width rules as applied on .slider_slide back in the Sliders Basics chapter. Here are those rules:

.slider_slide {
   width: 100%;
   max-width: 900px;
   display: none;
}

Doing so will keep the width of the progress bar in sync with the slider's slides.

Now, we could go on and write the following CSS to accomplish this idea:

.slider_progress-bar {
   width: 100%;
   max-width: 900px;
}

But we won't.

Instead, we'll refactor the CSS.

Rather than applying width and max-width to .slider_slide as we've been doing thus far in this tutorial, we'll now apply it on .slider directly. Then, we'll remove max-width from .slider_slide and just keep width: 100% on it. With this, .slider_progress-bar will also need the width: 100% declaration only.

This leads to the following CSS:

.slider {
   width: 100%;
   max-width: 900px;
}

.slider_slide {
   width: 100%;
   display: none;
}

/* ... */

.slider_progress-bar {
   width: 100%;
}

.slider_slide still has the width: 100% declaration applied so that it could fill in all the width of its parent container i.e. .slider.

Make sure to update your CSS to this new piece of code for every program from now on.

The benefit of taking this approach is that we only have to worry about setting the width of .slider (i.e. the main container) — each and everything therein will adjust itself automatically.

A discussion on refactoring

Refactoring, which simply means to rewrite a piece of code partially or completely, is typical in programming.

Initially, we might not design a piece of code such that it takes into account all the ways in which it would be used later on. But as soon as we add a particular feature to it, we might feel that it doesn't fit in nicely with the existing code base.

At this point we go on and refactor the code (i.e. change it) to easily blend in with the new feature while making sure that everything is efficient, well-written and flexible for further changes.

In our case, initially we didn't gave a custom width and max-width to .slider back when we created the most elementary slider in Sliders Basics. Rather we applied both of these styles to .slider_slide.

However, now that we want to give a progress bar to our slider, we feel that changing the CSS slightly would be really beneficial in the longer run.

Alright, with this done, let's now focus on the progress bar's actual styles.

Besides the width, it needs a height and a background color. Let's get done with this first:

.slider_progress-bar {
   width: 100%;
   height: 5px;
   background-color: #eee;
}

Similar to this, the .slider_progress-bar_bar element needs a custom width, height and background color as well.

As for the width, that'll keep on changing starting from 0, likewise we'll set width: 0 on the element. Moreover, its height will be 100% to follow the same height as of its container .slider_progress-bar_bar. As per the background color, we'll set it to a slightly dark grey color.

Altogether, this gives us the following code:

.slider_progress-bar {
   width: 100%;
   height: 7px;
   background-color: #eee;
}

.slider_progress-bar_bar {
   width: 0;
   height: 100%;
   background-color: #555;
}

In the live example below, we have a slider with a progress bar, whereby the inline style width: 40% is given to .slider_progress-bar_bar in order to show how would the progress bar look upon getting filled.

Live Example

Perfect.

Designing the logic

It's amazing to know that we have successfully written the HTML and CSS for the progress bar as it'll go in our slider. However, we haven't yet crafted the logic for it, let alone its testing and debugging.

So what do you think? How will the progress bar work?

There are essentially three concerns for a progress bar in our slider, as listed below:

  1. The progress bar should fill up with time.
  2. Once it fills up completely, the slider must navigate.
  3. After the slider's navigation, the progress bar must reset to 0% width.

Now after seeing this, it's our job to think about each of these things one-by-one i.e. how to write code to accomplish them.

Fortunately, they are all fairly easy to do.

To fill up the progress bar, we can either use a CSS animation on .slider_progress-bar_bar starting at the state width: 0 and ending at the state width: 100%; or even a CSS transition on the same element, going to the state width: 100%.

As for the second concern, that is navigating the slider when the progress bar fills up, we can easily apply the code from the previous Slider Autoplay chapter (to automatically navigate the slider after every 4s) and set the animationDuration or transitionDuration property (whichever we use) on .slider_progress-bar_bar to 4s to keep both the progress bar and the auto-navigation in sync with each other.

The third concern of resetting the progress bar requires a bit of knowledge about CSS animations/transitions, browser repainting, and synchronous code execution. Henceforth, we'll defer it for now and just focus on getting the first two things done.

Now before we start coding anything, it's worthwhile to note that for the purposes of testing our progress bar prior to taking it to the actual slider program, we'll create a separate HTML page containing the progress bar and two buttons, one to start it and one to reset it, and then work entirely there.

This is done so that we can solely focus on developing the logic of the progress bar and be sure that it works alright before embedding it into the actual slider.

Here's the HTML and JavaScript setup we've used:

<div class="slider_progress-bar">
   <div class="slider_progress-bar_bar"></div>
</div>
<p>
   <button id="start">Start</button>
   <button id="stop">Stop</button>
   <button id="reset">Reset</button>
</p>
var progressBarElement = document.querySelector('.slider_progress-bar_bar');
var startButtonElement = document.querySelector('#start');
var stopButtonElement = document.querySelector('#stop');
var resetButtonElement = document.querySelector('#reset');

startButtonElement.onclick = function(e) {};

resetButtonElement.onclick = function(e) {};

stopButtonElement.onclick = function(e) {};

Live Example

The Start button starts the progress bar while the Reset button resets it to the very beginning.

With this done, let's now get to the discussion.

As stated before, there are mainly two ways to get the progress bar to fill up using CSS, i.e. either via animations or transitions. We'll go with the latter since it's a little bit easier to set up as compared to setting up CSS animations.

Coming back to the progress bar, step 1 is to write some code to fill up the bar. This can very easily be done by creating a class to represent this moving state of the progress bar. Let's call it .slider_progress-bar_bar--moving.

Once again, this comes from the BEM naming convention that we're using in our slider. The moving modifier applies to .slider_progress-bar_bar and signals that it is currently in progression.

In this moving state, the width of .slider_progress-bar_bar is set to 100% and a transition declaration is also applied to it.

Shown below is the CSS code defining .slider_progress-bar_bar--moving:

.slider_progress-bar_bar--moving {
   transition: 4s linear;
   width: 100%;
}

In order to make the progress bar move, we ought to apply this class to .slider_progress-bar_bar.

This is done by the Start button on our testing page as shown below:

startButtonElement.onclick = function(e) {
   progressBarElement.classList.add('slider_progress-bar_bar--moving');
};

Here's a live example:

Live Example

Great. As we press the Start button, the progress bar starts moving.

Now, it's time for the next concern — making sure that the time after which the slider navigates is exactly the same time in which the progress bar gets filled up completely.

Well, recall that the time, in milliseconds, after which the slider navigates is represented by the property Autoplay.interval. In the previous chapter, we kept its value at 4000, which simply means 4s. That's exactly the time we chose for the duration of transition in the CSS code above.

And so this means that we have already addressed the second concern as well. At the end of 4s, the progress bar would be completely filled up and the slider would begin its navigation.

Perfect.

Now, it's time to take a look at the third concern — stopping the progress bar.

The idea is that when the progress bar is moving and still in between its completion, if the slider is navigated manually by using the ← Previous or Next → buttons, the progress bar has to be put to a halt and then restarted from point 0. In order to do this whole thing, we first need to be able to stop the progress bar in the middle of its movement.

How to accomplish this?

It's really simple. Just remove the class .slider_progress-bar_bar--moving.

In the code below, we try doing this inside the handler of the Stop button on our testing page:

stopButtonElement.onclick = function(e) {
   progressBarElement.classList.remove('slider_progress-bar_bar--moving');
};

Live Example

As we press the Stop button, the progress bar comes to a halt immediately. Good job!

The Stop button's handler above gives a rough, snappy end to the progress bar. Needless to say, we can surely improve this snappy behavior by fading out the moving bar when Stop is pressed. Try doing this as an exercise.

Now, it's time to address the very last concern of the progress bar — resetting the bar to a width of 0.

By definition here, 'resetting' is simply to stop the progress bar and then start it from point 0. Seems that we have already done this with the handlers of the Start and Stop buttons.

Let's try resetting the progress bar as follows:

resetButtonElement.onclick = function(e) {
   // First stop the progress bar.
   progressBarElement.classList.remove('slider_progress-bar_bar--moving');

   // Then, start it from width: 0.
   progressBarElement.classList.add('slider_progress-bar_bar--moving');
};

Quite expectedly, this doesn't work.

Live Example

Can you give the explanation why?

Well, here's where our knowledge of synchronous execution on the main thread and of browser repainting really matters. If we know about them all, we'd know the reason why the code above doesn't work as expected.

Let's see why...

When the first statement in line 3 above is executed, the given class is actually removed from .slider_progress-bar_bar. Next up, when the second statement in line 6 is executed, the class is added back again.

In the meanwhile as this code is being executed, nothing else can happen. That's because JavaScript gets executed on the main browser thread where repainting also happens.

Repainting is the process of drawing the graphics of the webpage on the screen based on its HTML markup and the CSS styles applied therein.

All transitions and animations, including their initial and final state computations, are performed right in this repainting stage.

Now what happens in the code above is that we remove and immediately add back the class 'slider_progress-bar_bar--moving' to the moving bar element. Then, when the next repainting routine occurs, the rendering engine sees no change in this element — previously it had the 'slider_progress-bar_bar--moving' class and even now it has it.

Likewise, it performs NO transition.

To solve this, we simply need to make sure that the class re-addition happens after the repainting routine.

So how to do this?

Here comes the knowledge of setTimeout() to the rescue.

To read more about setTimeout(), head over to the chapter JavaScript Timers in our exhaustive JavaScript course.

setTimeout() is a means of executing a function asynchronously after a given time span. Because the function executes asynchronously, we can perform certain operations in it without blocking the main thread while the rest of the code executes to completion.

The use of setTimeout() in our case is that we could move the last class re-addition statement in the code above inside a function and then provide this function to setTimeout().

Doing so would make sure that when the class 'slider_progress-bar_bar--moving' is removed from the respective element, the next thing is a browser repaint, followed by the execution of the function passed in to setTimeout() before.

Let's try simple method:

resetButtonElement.onclick = function(e) {
   // First stop the progress bar.
   progressBarElement.classList.remove('slider_progress-bar_bar--moving');

   // Then, start it from width: 0, after the next browser repaint.
   setTimeout(function() {
      progressBarElement.classList.add('slider_progress-bar_bar--moving');
   }, 0);
};

Live Example

This time, as we press the Reset button, the progress bar restarts from the very beginning and approaches the end. Just as we desired.

Now here's a catch: the argument 0 supplied to setTimeout() doesn't always work out nice when the browser is performing repainting routines. Sometimes, as we've witnessed on our devices, the progress bar doesn't reset.

This happens because the zero delay timeout sometimes gets merged with the next repainting routine and just acts as if it were a mere synchronous statement. This is solely the browser's decision as to whether treat a zero-delay timeout this way or not.

Stating it once again, although the code above might work perfectly on your device for quite a while, it might be erroneous on another device or another browser. The solution is very very simple — in fact, we ought to change just two lines.

That is, instead of providing 0 as the argument to setTimeout(), we instead go with 500.

In the code below, we make this very change:

resetButtonElement.onclick = function(e) {
   progressBarElement.classList.remove('slider_progress-bar_bar--moving');

   setTimeout(function() {
      progressBarElement.classList.add('slider_progress-bar_bar--moving');
}, 500); };
We could also go with 100, 200, 300 or any other small value that's a bit further apart from 0.

This means that after pressing the Start button, or even the Reset button, the progress bar starts after a delay of 500ms.

To counter the lag introduced by this delay, we have to reduce the transition duration given earlier to .slider_progress-bar_bar--moving to 3.5s (4000ms - 500ms = 3500ms = 3.5s).

This is accomplished below:

.slider_progress-bar_bar--moving {
transition: 3.5s linear; width: 100%; }

The transition comes into action after exactly 0.5s (500ms) and takes a time span of 3.5s to altogether span a duration of 4s which is currently the exact same time after which the slider navigates automatically.

Great.

With the 500ms delay added, now we also need to focus on the handler of the Stop button. That is, any pending timeout set by a previous interaction with Start or Reset ought to be cleared up in the handler of Stop.

For this, we obviously need a global variable in order to keep track of the timer ID returned by setTimeout(). Let's call it timerId.

Here's the modified code:

var timerId = null; /* ... */ stopButtonElement.onclick = function(e) { progressBarElement.classList.remove('slider_progress-bar_bar--moving');
clearTimeout(timerId); } resetButtonElement.onclick = function(e) { progressBarElement.classList.remove('slider_progress-bar_bar--moving');
clearTimeout(timerId); timerId = setTimeout(function() { progressBarElement.classList.add('slider_progress-bar_bar--moving'); }, 500); };

Simple, wasn't this?

So at this point, given that we have tested the progress bar thoroughly in isolation, it's time to finally incorporate its logic into our slider.

Enjoyed reading so far? 👌

Leave out a review for this chapter to us by sending a message to our Instagram page @codeguage.

Send message

Incorporating into our slider

In the section above, we tested a progress bar in isolation on a separate HTML page by three buttons, namely Start, Stop and Reset.

Now that the progress bar is fully-functional, we ought to take it into our slider.

To begin with, as we did in the previous chapter, we'll start by creating a ProgressBar namespace to hold all the functionalities of the progress bar.

But what are the functionalities?

Well, they're just the same functionalities that the buttons did in the testing examples above, i.e. to start, stop, and reset the progress bar.

We'll give these as the methods start(), stop() and reset(), respectively.

Here's the code so far:

var ProgressBar = {
   start: function() {},

   stop: function() {},

   reset: function() {}
};

Moving on, since a reference to the .slider_progress-bar_bar element is required in each of these methods, we'll create a property element to hold it; and also create a property timerId to hold the return value of setTimeout().

Here's the code:

var ProgressBar = {
   element: document.getElementsByClassName('slider_progress-bar_bar')[0],
   timerId: null,

   start: function() {},

   stop: function() {},

   reset: function() {}
};

What is left now is only to fill up the given methods with real code. And that's not difficult since we've already done that above. It's just a matter of rewriting those statements, but this time in a better way.

Consider the following code:

var ProgressBar = {
   element: document.getElementsByClassName('slider_progress-bar_bar')[0],
   timerId: null,

   start: function() {
      var _this = this;
      this.timerId = setTimeout(function() {
         _this.element.classList.add('slider_progress-bar_bar--moving');
      }, 0);
   },

   stop: function() {
      this.element.classList.remove('slider_progress-bar_bar--moving');
      clearTimeout(this.timerId);
   },

   reset: function() {
      this.stop();
      this.start();
   }
};

This gets almost all of the job done, with only a few things remaining.

One is to indicate the desire of having a progress bar in the slider, similar to the desire of having autoplay enabled in the slider which was done using the Boolean variable enableAutoplay in the previous chapter.

Following the same naming as before, we'll create a Boolean variable enableProgressBar to simply denote whether a progress bar is desired in the slider or not.

Unlike enableAutoplay, we won't have to manually set it to a given value. Rather, it'll detect the presence of the .slider_progress_bar_bar element, and assume that one is desired if the element exists.

The code below accomplishes this very elegantly using the ProgressBar.element property created above:

var ProgressBar = {
   element: document.getElementsByClassName('slider_progress-bar_bar')[0],
   timerId: null,

   start: function() { /* ... */},
   stop: function() { /* ... */},
   reset: function() { /* ... */},
};

/* ... */

var enableAutoplay = true;
var enableProgressBar = Boolean(ProgressBar.element);

If ProgressBar.element contains an element node, it gets coerced to the Boolean true indicating that yes a progress bar is desired in the slider. Otherwise, it's coerced to false.

Another thing to address is the fact that with enableProgressBar set to true, what should the status of enableAutoplay be.

That is, should we manually enforce enableAutoplay to be true when enableProgressBar is true or be a bit less strict and assert that it'll be true, although it really might not?

We'll go with the former here. The reason is because is seems more sensible to enforce enableAutoplay to be true when enableProgressBar is true as having a progress bar merely implies autoplay.

The idea is to simply check if enableProgressBar is true, and if it is, then further check if enableAutoplay is true as well. If this is not the case, an error is thrown signalling the problem to the developer:

var ProgressBar = {
   element: document.getElementsByClassName('slider_progress-bar_bar')[0],

   start: function() { /* ... */},
   stop: function() { /* ... */},
   reset: function() { /* ... */},
};

/* ... */

var enableProgressBar = Boolean(ProgressBar.element);

if (enableProgressBar && !enableAutoplay) {
   throw new Error('enableAutoplay must be true when a progress bar is desired.');
}

This completes the logic of our progress bar.

However, it still doesn't start it with the page load, and neither does it reset it with each new slider navigation.

Let's get dealt with these two things as well:

Inside the if conditional, where we check for enableAutoplay from the previous chapter, we include a second check for enableProgressBar and call ProgressBar.start() if it's true.

This is shown as follows:

/* ... */

if (enableAutoplay) {
   Autoplay.start();

   if (enableProgressBar) {
      ProgressBar.start();
   }
}

With this done, the next thing is to reset the progress bar upon the slider's navigation by calling ProgressBar.reset() right inside the conditional in navigateSlider() where Autoplay.reset() is also called.

Shown below is the new code:

/* ... */

function navigateSlider() {
   if (enableAutoplay) {
      Autoplay.reset();

      if (enableProgressBar) {
         ProgressBar.reset();
      }
   }

   /* ... */
}

/* ... */

Let's now try it out:

Live Example

It just works flawlessly.

Remember that in order to give a progress bar to our slider, we have to manually include the whole markup of the progress bar as given at the start of this chapter.