Spicing up the slider

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. This spice will be a good one for the outcome of this slider recipe!

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.

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

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 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 nav button clicks.

Broken Progess 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 markup

Review the progress bar shown above and try to figure out its basic markup - how many elements does it require; what styles will be needed to transform it into a running progress bar and so on.

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 running bar. Then obviously, within this container, we will place the actual running bar and consequently limit its width to the width of its parent.

We'll classify the container as .progress-bar, and the moving bar as .bar, leading to the following HTML markup:

<div class="progress-bar">
    <div class="bar"></div>
</div>
As you'll soon see, we won't use this markup as it is, inside our slider's container - instead we will only use the .progress-bar container, omitting its child .bar. We'll detect the container using JavaScript and likewise append the running bar within it.

Now that we've layed out the basic HTML for our progress bar, where do you think it should go within the markup of our slider (constructed in the Basic Understanding 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-cont">
    <div class="progress-bar"></div>
    <div class="slider">
        <div class="slides">Slide 1</div>
        <div class="slides">Slide 2</div>
        <div class="slides">Slide 3</div>
    </div>
    <button class="slider-nav">&lt; Back</button>
    <button class="slider-nav">Forward &gt;</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:

.progress-bar {    
    height: 5px; /* height for the bar */
    background: #eaeaea;
    position: relative; /* make .bar scale relative to this */
}
.progress-bar .bar {
    height: 100%; /* fill its 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 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

First of all the progress bar needs to be in sync with the navigation of the slider i.e the duration in which .bar fills up and the interval after which the navigation is made MUST be the same, otherwise one thing will happen before the other and therefore break up the harmony of the progress bar.

Now accomplishing this, in practice, is quite difficult. If we get our time variable indeed indicate the time interval before navigating the slider then we'll have to apply this same time to the transitionDuration / animationDuration style property on the .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 .bar using the style property
  2. Add a class moving on .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 intial 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 .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 .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 to pause the progress bar in its current width and then replay 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 solve our progress bar problem.

The idea is very simple:

Increment .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 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. Starting the bar to progress (move) upto 100% width
  2. Resetting the bar to its original 0% width

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

start() will call the requestAnimationFrame() method and pass it a callback which will increment .bar's width by a certain value each time it's invoked. This 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 currentWidth, which will save the bar's current width. This property will then be used to assign a value to bar.style.width.

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() will simply set currentWidth back to 0 and thus get the bar to restart from its initial zero point.

Lastly, to easily access the .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 .bar
    target: null,

    // the amount by which .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 .bar
    currentWidth: 0,

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

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

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

Following are some details to get you started:

  1. Take widthIncrement as 0.5.
  2. start() shall call requestAnimationFrame() and pass it another method move() that actually deals with incrementing .bar's width.
  3. move() is the main function here - it calls rAF recursively and resets the bar once currentWidth reaches 100.

The method reset() and start 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.

First we'll check for currentWidth >= 100and thereby call reset() if it evaluates to true.

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

var ProgressBar = {
    // rest of the code

    reset: function() {
        this.currentWidth = 0;
    },
    start: function() {
        this.id = requestAnimationFrame(this.move);
    },
    move: function() {
        var self = ProgressBar;
        if (self.currentWidth >= 100)
            this.reset();
        self.currentWidth += self.widthIncrement;
        self.target.style.width = self.currentWidth + "%";
        self.id = requestAnimationFrame(self.move);
    }
}
Here we've assumed that target holds a reference to .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 start() 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="progress-bar">
    <div class="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 three methods and handful of properties. Finally create two buttons to test for these methods - one should call reset() and the other should call start().

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

Test Progess 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 navigations in our slider.

In simple words, first we chose a time of our choice, put that in the property Automation.time 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 a consistent interval. Hence 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.

As you now know, blending this rAF logic with our old time variable will be a fairly strenous activity to do, with no practical advantages whatsoever.

First we'll have to do some rough math to come up with a value for widthIncrement that fills the bar in sync with time. Then even with this in place we'll have to further check for .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 up with similar issues. Therefore what we'll do instead is:

Make the progess bar govern the time of navigation - NOT the time to govern the progress bar.

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

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.currentWidth >= 100) {
            counter++;
            navigateSlider();
        }
        self.currentWidth += self.widthIncrement;
        self.target.style.width = self.currentWidth + "%";
        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 bar's width reaches 100 we'll 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 .progress-bar into the variable progressBar (if it exists, or else the value undefined).

Then we lay a conditional to check whether either of automate or progressBar is true. Inside this conditional we specifically check for progressBar and if it returns true, carry out the following tasks:

  1. Create an element .bar
  2. Append it to progressBar
  3. Assign it to ProgressBar.target
  4. Call ProgressBar.start()

Otherwise we simply call Automation.start().

Consider the code below:

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

if (automate || progressBar) {
    // specifically check for progressBar
    if (progressBar) {
        var bar = document.createElement("div");
        bar.className = "bar";
        progressBar.appendChild(bar);
        ProgressBar.target = bar;
        ProgressBar.start();
    }
    else {
        Automation.start()
    }
}

This will get either the setInterval() or requestAnimationFrame() method take over and start the slider's automation.

To end with, we create a global function resetAutomation() that'll be used inside navigateSlider() to call ProgressBar.reset() if progressBar exists, otherwise Automation.reset().

function resetAutomation() {
    progressBar ? ProgressBar.reset() : Automation.reset();
}

function navigateSlider() {
    // counter check code

    if (automate || progressBar)
        resetAutomation();

    // rest of the code
}

This will now get the reset logic work smoothly - if progressBar is true we'll simply reset it rather than resetting the setInterval() function for Automation.reset().

We double-check for progressBar to confirm whether it actually has been set, and if it is then we implement its functionalities instead of implementing the ones for Automation, even if automate is set to true!

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!