Introduction

Taking our Automated Slider from the previous chapter a step further, we will now be giving it a progress bar to track when the current slide is about to be navigated.

You might already be familiar with the idea of progress bars back from when you installed some piece of software on your operating system - the installer that came in with the software package might've probably some horizontal bars filling up, as the chunks of data were being downloaded.

Click the button below to fill up this progress bar:

The idea of tracking progress works well in situations like downloads and uploads. However, it can even fit well within our very own slider, and tell us exactly when it's about to be navigated.

Evaluating possibilites

The progress bar we'll be creating in a while will need to meet the following two requirements:

  1. It should complete in Automation.time milliseconds, (a property created in the previous chapter for the duration of each interval).
  2. It should reset once Automation.reset() is called, and likewise begin progressing from the very beginning.

The first requirement of filling the progress bar in Automation.time milliseconds can be easily achieved using CSS animations (or even transitions) by changing the bar's width from 0 to 100% in the given duration.

However if we rely on CSS animations, the second requirement of resetting the bar couldn't be accomplished easily.

Check out the following example of a slider with a progress bar driven by CSS animations, and notice how it doesn't reset on the click of the navigation buttons.

Broken Progress Bar Slider

Now indeed, it's not totally impossible to get CSS animations power up our progress bar, but at the same time not even close to 0.1% easy. A much better and simpler approach would be to use JavaScript instead, as we shall see below.

But before moving into the JavaScript let's first consider the basic HTML and CSS for this progress bar.

Basic HTML markup

Review the progress bar shown at the start of this chapter and try to figure out its basic markup — how many elements does it require.

Assuming you've given it a go, let's now decode the markup for an elementary progress bar.

First, we'll need a <div> element to act as the container for the actual moving bar. Then obviously, within this container, we'll place the moving bar as another <div>.

We'll classify the container as .slider_progress-bar, and the moving bar as .slider_progress-bar_bar, based on the BEM naming convention; which we've been using in our previous naming concerns as well.

This leads us to the following HTML markup:

<div class="slider_progress-bar">
    <div class="slider_progress-bar_bar"></div>
</div>
As you'll see soon, we won't use this markup as it is, inside our slider's markup — instead, we will only use the .slider_progress-bar container and omit its child .slider_progress-bar_bar.

We'll detect the container using JavaScript and if it exists, only then append the moving bar within it.

Now that we've layed out the HTML for our progress bar, where do you think it should go within the markup of our slider (constructed in the Slider Basics chapter).

It's very simple - the progress bar will go directly inside .slider-cont.

The element .slider only serves to encapsulate all the individual slides, and likewise won't meet our criteria - rather it's .slider-cont that encapsulates all the features of our slider and hence meets our requirement rightaway.

Following is an illustration:

<div class="slider">
    <div class="slider_progress-bar"></div>
    <div class="slider_slides-cont">
        <div class="slider_slide">Slide 1</div>
        <div class="slider_slide">Slide 2</div>
        <div class="slider_slide">Slide 3</div>
    </div>
<button class="slider_nav">&larr; Back</button>
<button class="slider_nav">Forward &rarr;</button>
</div>

Styling with CSS

Now let's give this progress bar the styles necessary to actually make it look like one. We'll use a very basic styling logic, keeping everything simple and minimal.

Follow along the comments in the code below to understand the purpose of each style declaration:

.slider_progress-bar {    
    height: 5px; /* height for the bar */
    background: #eee;
    position: relative;
}
.slider_progress-bar .slider_progress-bar_bar {
    height: 100%; /* fill container's height */
    background: black;
    width: 0; /* initially the bar has NO width */
}

Problems with CSS transitions

Over with all but the last concern of this chapter, we will now get some intuition behind constructing the JavaScript for our progress bar.

It will not be a straightforward trek so be ready for it — we're about to have a long discussion on web animations in general, and see the pros and cons of each option; to truly appreciate the way we'll take to power the progress bar above.

Let's start by analysing CSS transitions/animations for running up our progress bar.

As we've said before, using CSS transitions/animations to power up our progress bar comes with a lot more cons than its pros. Following are a couple of them.

Difficult to keep in sync

As we know, the progress bar needs to be in sync with the navigation of the slider i.e the time in which .slider_progress-bar_bar fills up and the time after which the navigation is made MUST be the same, otherwise one thing will happen before the other and, as a result, break up the harmony of the progress bar.

Now accomplishing this, in practice, is quite difficult.

If we say that Automation.timeInterval indicates the time interval before navigating the slider, then we'll have to apply this same interval to the transitionDuration (or animationDuration) style property on the .slider_progress-bar_bar element.

Doing this isn't what the difficulty is about, but rather being able to manage it.

How will you pause a progress bar powered by CSS animations; how will you reset it back to its 0% point; or the most involved - how will you get the bar to start moving rightaway, as soon as the page loads?

Understanding the last question - essentially, there are two ways of getting the bar to fill:

  1. Add the style width = "100%" manually to .slider_progress-bar_bar using the style property
  2. Add a class moving on .slider_progress-bar_bar that in turn applies the rule width: 100%

Now if you have a little bit of experience in working with CSS transitions from within JavaScript, then you'll realise the fact that both these choices would require an asynchronous call to initiate the width: 100% final transition state.

A direct synchronous call would otherwise have no effect, and will register width: 100% as the initial transition state (rather than the final one).

Tedious to reset to 0% width

Apart from this, we yet face another trouble in going the CSS way, which happens when we need to reset the bar's width back to 0%.

Assuming time is 5s, this would mean that .slider_progress-bar_bar's transitionDuration will also be set to 5s. Once this time passes by, we'll obviously think of directly resetting the width to 0% using the code below, not noticing one problem in it.

// execute the following code to reset bar's width
bar.style.width = "0%";

Can you figure out this problem? Let's see your analysis skills!

Here's a hint to help you: The problem has to do with CSS transitions!

The problem is discussed as follows:

Supposing that the duration of the transition is 5s, width: 0% would likewise be applied in a span of 5s. Ideally we want to reset the bar's width in a span of 0s (or some other small value), but at least with a duration of 5s we aren't even close to solving this problem.

A novice developer would say to just do the following, thinking as if it would solve the problem:

// make any style change happen in 0s
bar.style.transitionDuration = "0s";

// then reset width to 0
bar.style.width = "0%";

// and finally reimpose the previous transition-duration
bar.style.transitionDuration = time + "s";

Surprisingly for the developer, even this won't solve the problem!

A browser will typically first execute all this JavaScript, and then move on to carry out the repainting routines. This would mean that our transition duration will remain as 5s and hence our width will go to zero in a span of 5s.

What we can think of rather is to delay the resetting of transitionDuration:

bar.style.transitionDuration = "0s";
bar.style.width = "0%";

// reimpose the previous transition-duration after 500ms
setTimeout(function() {
    bar.style.transitionDuration = time + "s";
}, 500);

But to amaze you, even this has its own problems!

If we delay the timer after 5s (for let's say 500ms), then the transition won't remain in sync with the next navigation. The delay causes a lag of 500ms, which means that the next navigation will fire when the transition is in its 4.5s frame and where .slider_progress-bar_bar's width won't be 100%!

Even if you come up with a solution to this issue, the list of limitations in using CSS transitions won't be completely dealt with. How will you solve the problem of pausing the progress bar in its current width and then replaying it from where we paused it?

If you think on all these problems for a while, you'll surely ascertain that using CSS isn't the solution for powering up our slider's progress bar.

Instead what we need is a simpler, easier, and compact way to solve this arguably easy task. And that is using requestAnimationFrame().

A modern API

requestAnimationFrame() is a fairly recent API to power web animations using callback functions. It operates similar to setTimeout(), except for that it doesn't have a second, time delay argument.

The reason is because requestAnimationFrame() queues a callback to be invoked on the next browser repaint - which typically happens at 16.7ms intervals (arising from the well-known 60fps frame rate).

Now because of its simplicity and efficiency, we will employ requestAnimationFrame() to power up our progress bar.

The idea is very simple:

Increment .slider_progress-bar_bar's width by a certain amount on each call of requestAnimationFrame() and once it reaches 100%, reset it back to 0%.

Let's first implement these functionalities in an isolated progress bar and once they work well, take them to our slider.

As we did in the previous Slider Autoplay chapter, here we'll once again encapsulate all the functionalities of our progress bar inside a variable ProgressBar.

Recall that doing so helps prevent crowding up the global namespace with identifiers, which in turn helps to improve the program's extensibility and readability.

These functionalities include:

  1. Moving the bar to progress upto 100% width
  2. Resetting the bar to its original 0% width

Let's name the first functionality as move and the second one as reset — both being methods on the ProgressBar object.

move() holds the logic to move the progress bar. It is passed onto the requestAnimationFrame() function and likewise, called on each browser repaint routine, where it increments the width of the progress bar.

This increment value could be large, for example 2, 3, to move the bar quickly or small like 0.1, 0.2, to move the bar slowly. Owing to the fact that this is a customisable attribute of a progress bar, which we may alter according to our needs, we'll assign it to a new widthIncrement property of ProgressBar.

Similarly, to track the width of the bar, we'll create another numerical property width, which will save the bar's current width. This property will then be used to assign a value to bar.style.width.

Apart from this, the move() method also checks when the bar's width reaches 100%, at which point it resets it by calling the reset() method.

Furthermore, because rAF logic is involved in this discussion, we'll create a property id to temporarily save the ID returned by requestAnimationFrame() which will help in canceling it in future (if we ever need to do so).

The method reset() simply sets width back to 0 and thus gets the bar to restart from its initial zero point. It doesn't restart the the animation put up by requestAnimationFrame() — it just resets the bar's width.

Lastly, to easily access the .slider_progress-bar_bar element, we'll hold a reference to it inside the property target.

Summing all this up we get the following code:

var ProgressBar = {
    // saves the ID of requestAnimationFrame()
    id: null,

    // holds a reference to .slider_progress-bar_bar
    target: null,

    // the amount by which .slider_progress-bar_bar's width shall increment on each repaint
    // for now, we use the dummy value 0.5
    widthIncrement: 0.5,

    // holds the current width of .slider_progress-bar_bar
    width: 0,

    // methods
    move: function() {},
    reset: function() {}
}

Let's now focus on the last two methods here.

Construct definitions for the methods move() and reset().

Increment ProgressBar.width by ProgressBar.widthIncrement, and then set the progress bar's width to this new value.

The methods start() and reset() perform one-line actions as you can read in the section above, and so we won't explore them in detail here. Defining move() is what our main job is.

In move(), first we'll check for width >= 100 and thereby call reset() if it evaluates to true.

Then, regardless of this condition, we'll increment width using widthIncrement, modify the width of .slider_progress-bar_bar by assigning it the value width + "%", and finally reinvoke the requestAnimationFrame() method.

var ProgressBar = {
// rest of the code

reset: function() {
    this.width = 0;
},
start: function() {
    this.id = requestAnimationFrame(this.move);
},
move: function() {
    var self = ProgressBar;
    if (self.width >= 100)
        self.reset();
    }
    self.width += self.widthIncrement;
    self.target.style.width = self.width + "%";
    self.id = requestAnimationFrame(self.move);
}
Here we've assumed that target holds a reference to .slider_progress-bar_bar, but haven't actually written the code to do so. This will be accomplished in the section below.

Now that we know how the methods move() and reset() operate, we can finally test them on an isolated progress bar and see whether they are working as expected.

Go ahead, create a separate HTML file and put in the markup for the progress bar we wrote above, along with the CSS. Following is a review of this markup:

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

With this done, next write the JavaScript code for brining the progress bar to life i.e create the ProgressBar object with its two methods and handful of properties. Finally create two buttons to test for these methods — one should call move() and the other should call reset().

Let's take a look over what we've accomplished as of yet in our progress bar.

Test Progress Bar

As you can see, this works amazingly well and hence, now enables us to proceed with other concerns in incorporating this functionality into our slider.

Thinking efficiently

Uptil now, starting from the previous Slider Autoplay chapter, we've had fixed time intervals between the navigations of our slider.

First we chose a time of our choice, put that in the property Automation.timeInterval and finally passed this to setInterval() to create a timer with the respective interval.

This idea worked well when we had no progress bars, but with one present we can't continue using it. There's a technical reason to it:

In requestAnimationFrame(), managing state is way easier than managing time.

It would require only a straightforward conditional statement to check for a state such as width: 100% or opacity: 0 and proceed likewise.

However, to check for a given time (5 seconds in this case), we'll have to be a bit tedious in our code - save the initial time, subtract it from each timestamp corresponding to the current frame, then check whether it is equal to time and so on.

What we're simply trying to highlight over here is that requestAnimationFrame() doesn't work like setInterval(), which is, otherwise called after fixed intervals. Instead, rAF is called whenever a next browser repaint is to be made, and doesn't always fire at consistent intervals.

Subsetting rAF into a time boundary is quite difficult and not what the API is meant for.

In the isolated progress bar we created above, we had a check for width >= 100 and once that occured we called the reset() method, by setting width to 0.

If we were to blend this rAF logic with our old time-governed setInterval() mechanism, it would've been a strenous task for us to write the code to do so, with no practical advantages whatsoever.

First we would've had to do some rough math and come up with a value for widthIncrement (from this rAF logic) that filled the progress bar in sync with timeInterval (from the setInterval() logic). Even with this in place, we would've had to further check for .slider_progress-bar_bar's width and force it to 100% if it isn't at this mark.

If you experiment around with all this for a while you'll come to the same conclusion — that it is not really precise to manage time intervals using the rAF API.

What we'll do instead is this:

Make the progress bar govern the autoplay feature — NOT the autoplay feature to govern the progress bar.

What this means is that as soon as our progress bar reaches 100% width, we navigate the slider by calling navigateSlider(), regardless of how long it takes this to happen.

Now our progress bar doesn't have to fill up in the time interval set by Automation.timeInterval; but instead, the autoplay feature has come into action when the progress bar fills up completely.

Following we've made some modifications to our previous code to incorporate the aforementioned idea into it:

var ProgressBar = {
    // rest of the code

    move: function() {
        var self = ProgressBar;
        if (self.width >= 100) {
            newIndex++;
            navigateSlider();
        }
        self.width += self.widthIncrement;
        self.target.style.width = self.width + "%";
        self.id = requestAnimationFrame(self.move);
    }

    // rest of the code
}

As you can see here, we've shifted the navigation logic directly into move(). Whenever the progress bar reaches its end, we navigate the slider, which will in turn reset the bar.

The re-definition of navigateSlider() is covered in the next and final section below.

Final implementation

Wrapping up this long discussion, we'll now be finally developing the complete logic of a progress bar in our slider.

We begin by fetching the element .slider_progress-bar into the variable progressBar using getElementsByClassName(). If the element exists, its reference is saved in progressBar, otherwise the value undefined.

var progressBar = document.getElementsByClassName('slider_progress-bar')[0];
If you want to know how getElementsByClassName() works, head over to JavaScript HTML DOM — Selecting elements by class.

Next we redefine Automation.start() by setting up our progress bar mechanism in it, if progressBar evaluates to true.

The progress bar mechanism is detailed below.

  1. Create a <div> element and give it the class 'slider_progress-bar_bar'.
  2. Append it to progressBar.
  3. Assign it to ProgressBar.target.
  4. Call ProgressBar.move()

Otherwise, if progressBar is false (but automate is true), we fall back to our old setInterval() autoplay logic.

All this is summarised as follows:

var progressBar = document.getElementsByClassName("slider_progress-bar")[0];

var Automation = {
    /* ... */
    start: function() {
        if (progressBar) {
            var bar = document.createElement("div");
            bar.className = "slider_progress-bar_bar";
            progressBar.appendChild(bar);
            ProgressBar.target = bar;
            ProgressBar.move();
        }
        else {
            this.id = setInterval(function() {
                newIndex++;
                navigateSlider();
            }, this.timeInterval);
        }
    }
}

To end with, we redefine the Automation.reset(), by calling ProgressBar.reset() in it if progressBar exists, otherwise falling back to our old setInterval() autoplay logic:

Automation = {
    /* ... */                
    reset: function() {
        if (progressBar) {
            ProgressBar.reset();
        }
        else {
            clearInterval(this.id);
            this.start();
        }
    }
}

This will now get the reset logic work smoothly — if progressBar is true we'll go on and reset the progress bar rather than resetting the setInterval() function for Automation.reset().

And this marks an end to this long, but fruitful, discussion on giving a progress bar to our slider. Congradulations on reaching uptil here and learning literally a lot of stuff!

Check out the following live example.

Live Example

In conclusion

Verily you've spent a lot of your precious time on reading the tiny bits and pieces of this chapter, but not without some outcome!

At this point you know the limitation using CSS animations in JavaScript, the discipline to build a web component and how to namespace related functionalities under a wrapper object. Not only this but you even learnt a practical usage of requestAnimationFrame(), and how to work with it in general.

To test you understanding of this chapter we've got a quiz ready for you up next in line. Take it, see where you stand and review the concepts discussed here if the need be.

This chapter has surely ended, but not this course of sliders. Keep learning and make yourself more skillful as a programmer!