Create Responsive Slider with jQuery and CSS3 [ Awesome Guide ]

Responsive Slider with jQuery and CSS3

If front end development is one of your responsibilities then you must have already used one of many great jQuery slider plugins available online. If not, then it’s only a matter of time before you use one. There may come a time when you will need to create a slider of your own. In this tutorial, let’s learn how to create a simple responsive slider with jQuery and CSS3.

Here’s a sneak peek of what we are making. Use a modern browser as I have skipped all vendor prefixing!

Slider Features

Here’s a list of features we want in our slider

  • Display anything in the slider including images and text
  • Next and Previous navigation controls
  • Autoplay
  • Pagination controls
  • Developer friendly API for easy control

Slider HTML

Let’s start with the HTML. Here’s the complete HTML code

<div class="slider-box">
    <div class="slider" id="slider">
        <div class="slider__item">
            <img src="https://placeimg.com/960/540/animals" alt="">
            <h2 class="slider__item__title">Slide 1 Title</h2>
        </div>
        <div class="slider__item">
            <img src="https://placeimg.com/960/540/arch" alt="">
            <h2 class="slider__item__title">Slide 2 Title</h2>
        </div>
        <div class="slider__item">
            <img src="https://placeimg.com/960/540/nature" alt="">
            <h2 class="slider__item__title">Slide 3 Title</h2>
        </div>
        <div class="slider__item">
            <img src="https://placeimg.com/960/540/people" alt="">
            <h2 class="slider__item__title">Slide 4 Title</h2>
        </div>
        <div class="slider__item">
            <img src="https://placeimg.com/960/540/tech" alt="">
            <h2 class="slider__item__title">Slide 5 Title</h2>
        </div>
    </div>

    <div class="slider-nav">
        <div class="slider-nav__prev" id="prev"><i class="fa fa-angle-left"></i></div>
        <div class="slider-nav__next" id="next"><i class="fa fa-angle-right"></i></div>
        <div class="slider-nav__dots" id="dots"></div>
    </div>
</div>

Right at the top is a div element that acts as a wrapper for our slider and its controls.

<div class="slider-box">

Next up is our slider itself.

<div class="slider" id="slider">

Each slide item goes in their own div elements. Each slide contains an image and a header tag.

<div class="slider__item">
    <img src="https://placeimg.com/960/540/animals" alt="">
    <h2 class="slider__item__title">Slide 1 Title</h2>
</div>

The slider controls are independent. You can place them absolutely anywhere you want. We are simply grouping them inside our slider wrapper element.

<div class="slider-nav">
    ...
</div>

Inside the navigation, there are two div elements which will act as previous and next controls.

<div class="slider-nav__prev" id="prev"><i class="fa fa-angle-left"></i></div>
<div class="slider-nav__next" id="next"><i class="fa fa-angle-right"></i></div>

And another one, which acts as a placeholder for pagination controls.

<div class="slider-nav__dots" id="dots"></div>

Note that you can place controls anywhere you want.

Slider CSS

Now let’s have a look at our CSS. Here is the core CSS of our slider

.slider-box,
.slider__item {
    position: relative;
}

.slider {
    overflow: hidden;
}

.slider__canvas {
    transition:  transform 0.5s;
}

.slider__item {
    float: left;
}

We are using relative positioning for our slider wrapper and its elements.

.slider-box,
.slider__item {
    position: relative;
}

Next, is a very critical CSS for our slider which needs to have it’s overflow property set to hidden.

.slider {
    overflow: hidden;
}

After that, we are setting a CSS for an element which is not available in our HTML.

.slider__canvas {
    transition:  transform 0.5s;
}

The above element will act as the sliding canvas. Our slider will be powered by hardware accelerated CSS3 animations. You can control the slide animation time by setting the CSS animation-duration property.

And the final CSS sets the slide items float property. This is yet another critical CSS needed for our slider to work.

.slider__item {
    float: left;
}

The JS

And now, the most important stuff, the JS itself. Before we get to the code part, let me explain how this works. We will be creating a jQuery plugin. It’s the most simple and efficient way to make jQuery code reusable.

Our slider will be event driven. We will trigger custom events and act on those events. All features like navigation, autoplay will be controlled by triggering events on the slider element. This will fulfill our goal of creating a clean API for the developers

Elements floated to the left appear next to each other as long as the parent width is not exceeded. We will use this as our base. We will set the parent element width equal to the width of all slides. The parent element is slider__canvas. This element will be injected using JavaScript.

The slider animations will be powered by hardware accelerated CSS3 animations. We will move slider__canvas element using CSS3 translate3d. The animation duration, as mentioned earlier can be controlled by using CSS3.

Where needed, we will add additional active class to enable better animations and styling using CSS3.

Here is the complete JS code.

(function($) {

    'use strict';

    var pluginName = 'slider',
    defaults = {
        next: '.slider-nav__next',
        prev: '.slider-nav__prev',
        item: '.slider__item',
        dots: false,
        dotClass: 'slider__dot',
        autoplay: false,
        autoplayTime: 3000,
    };

    function slider(element, options) {
        this.$document = $(document);
        this.$window = $(window);
        this.$element = $(element);
        this.options = $.extend({}, defaults, options);
        this.init();
    };

    slider.prototype.init = function() {
        this.setup();
        this.attachEventHandlers();
        this.update();
    };

    slider.prototype.setup = function(argument) {
        this.$slides = this.$element.find(this.options.item);
        this.count = this.$slides.length;
        this.index = 0;

        this.$next = $(this.options.next);
        this.$prev = $(this.options.prev);

        this.$canvas = $(document.createElement('div'));
        this.$canvas.addClass('slider__canvas').appendTo(this.$element);
        this.$slides.appendTo(this.$canvas);

        this.$dots = $(this.options.dots);
        this.$dots.length && this.createDots();
    };

    slider.prototype.createDots = function() {
        var dots = [];
        for(var i = 0; i < this.count; i += 1) {
            dots[i] = '<span data-index="' + i + '" class="' + this.options.dotClass + '"></span>';
        }
        this.$dots.append(dots);
    }

    slider.prototype.attachEventHandlers = function(){
        this.$element.on('prev.slider', this.prev.bind(this));
        this.$document.on('click', this.options.prev, (function(e) {
            this.$element.trigger('prev.slider');
        }).bind(this));

        this.$element.on('next.slider', this.next.bind(this));
        this.$document.on('click', this.options.next, (function(e) {
            this.$element.trigger('next.slider');
        }).bind(this));

        this.$element.on('update.slider', this.update.bind(this));
        this.$window.on('resize load',(function(e) {
            this.$element.trigger('update.slider');
        }).bind(this));

        this.$element.on('jump.slider', this.jump.bind(this));
        this.$document.on('click', ('.'+this.options.dotClass), (function(e) {
            var index = parseInt($(e.target).attr('data-index'));
            this.$element.trigger('jump.slider', index);
        }).bind(this));

        this.$element.on('autoplay.slider', this.autoplay.bind(this));
        this.$element.on('autoplayOn.slider', this.autoplayOn.bind(this));
        this.$element.on('autoplayOff.slider', this.autoplayOff.bind(this));
        this.$element.bind('prev.slider next.slider jump.slider', this.autoplay.bind(this));
        this.options.autoplay && this.$element.trigger('autoplayOn.slider');
    };

    slider.prototype.next = function(e) {
        this.index = (this.index + 1) % this.count;
        this.slide();
    };

    slider.prototype.prev = function(e) {
        this.index = Math.abs(this.index - 1 + this.count) % this.count;
        this.slide();
    };

    slider.prototype.jump = function(e, index) {
        this.index = index % this.count;
        this.slide();
    }

    slider.prototype.autoplayOn = function(argument){
        this.options.autoplay = true;
        this.$element.trigger('autoplay.slider');
    };

    slider.prototype.autoplayOff = function() {
        this.autoplayClear();
        this.options.autoplay = false;
    }

    slider.prototype.autoplay = function(argument){
        this.autoplayClear();
        if (this.options.autoplay) {
            this.autoplayId = setTimeout((function() {
                this.$element.trigger('next.slider');
                this.$element.trigger('autoplay.slider');
            }).bind(this), this.options.autoplayTime);
        }
    };

    slider.prototype.autoplayClear = function() {
        this.autoplayId && clearTimeout(this.autoplayId);
    }

    slider.prototype.slide = function(index) {
        undefined == index && (index = this.index);
        var position = index * this.width * -1;
        this.$canvas.css({
            'transform' : 'translate3d(' + position + 'px, 0, 0)',
        });
        this.updateCssClass();
    };

    slider.prototype.update = function() {
        this.width = this.$element.width();
        this.$canvas.width(this.width * this.count);
        this.$slides.width(this.width);
        this.slide();
    };

    slider.prototype.updateCssClass = function() {
        this.$slides
        .removeClass('active')
        .eq(this.index)
            .addClass('active');

        this.$dots
        .find('.' + this.options.dotClass)
            .removeClass('active')
            .eq(this.index)
                .addClass('active');
    }

    $.fn[pluginName] = function(options) {
        return this.each(function() {
            !$.data(this, pluginName) && $.data(this, pluginName, new slider(this, options));
        });
    };

})(window.jQuery);

Now, the long explanation of the above…

Our JS code starts with a wrapper function that receives the global jQuery object.

(function($) {
    ...
})(window.jQuery);

Our code will execute in strict mode

'use strict';

We set two variables, one stores the plugin name while the other stores the default options for our plugin.

var pluginName = 'slider',
    defaults = {
        next: '.slider-nav__next',
        prev: '.slider-nav__prev',
        item: '.slider__item',
        dots: false,
        dotClass: 'slider__dot',
        autoplay: false,
        autoplayTime: 3000,
    };

Here’s what the default options represent

  • next – CSS selector for the element that will act as the next navigation element.
  • prev – CSS selector for the element that will act as the previous navigation element.
  • item – CSS selector for the slide elements.
  • dots – CSS selector for the element where pagination controls will be placed. Setting this to false disables pagination.
  • dotClass – When pagination is enabled, this class will be added to all pagination controls.
  • autoplay – Enable or disable auto-play by setting this to true of false respectively. This is disabled by default.
  • autoplayTime – Auto-play time in milliseconds.

Next, we have our constructor function

function slider(element, options) {
    this.$document = $(document);
    this.$window = $(window);
    this.$element = $(element);
    this.options = $.extend({}, defaults, options);
    this.init();
};

The above familiar looking constructor function caches the document, window and target slider element. It also merges the default options with the ones provided when initializing the plugin.

https://api.jquery.com/jquery.extend/

Finally, our constructor calls the init method to get things going. Looking at the init code, we see that it does nothing other than calling other functions.

slider.prototype.init = function() {
    this.setup();
    this.attachEventHandlers();
    this.update();
};

Here is the setup function

slider.prototype.setup = function(argument) {
    this.$slides = this.$element.find(this.options.item);
    this.count = this.$slides.length;
    this.index = 0;

    this.$next = $(this.options.next);
    this.$prev = $(this.options.prev);

    this.$canvas = $(document.createElement('div'));
    this.$canvas.addClass('slider__canvas').appendTo(this.$element);
    this.$slides.appendTo(this.$canvas);

    this.$dots = $(this.options.dots);
    this.$dots.length && this.createDots();
};

Let’s break it down…

Cache the slides

this.$slides = this.$element.find(this.options.item);

Create two variables, one to store the total number of slides and another one to store  the current slide index and set it to 0 (zero).

this.count = this.$slides.length;
this.index = 0;

Next, cache the next and previous navigation elements

this.$next = $(this.options.next);
this.$prev = $(this.options.prev);

After that, create the canvas and add the slides to the canvas

this.$canvas = $(document.createElement('div'));
this.$canvas.addClass('slider__canvas').appendTo(this.$element);
this.$slides.appendTo(this.$canvas);

Finally, cache the pagination container element and create pagination if the pagination container element exists.

this.$dots = $(this.options.dots);
this.$dots.length && this.createDots();

The pagination is created using createDots function

slider.prototype.createDots = function() {
    var dots = [];
    for(var i = 0; i < this.count; i += 1) {
        dots[i] = '<span data-index="' + i + '" class="' + this.options.dotClass + '"></span>';
    }
    this.$dots.append(dots);
}

When creating the pagination elements, the above code also adds an index data attribute to each pagination element which is later used to go to to the proper slide.

What follows next is attachEventHandlers – the one that umm… attaches all event handlers of our plugin. This is the longest running piece of crap code in our plugin.

slider.prototype.attachEventHandlers = function(){
    this.$element.on('prev.slider', this.prev.bind(this));
    this.$document.on('click', this.options.prev, (function(e) {
        this.$element.trigger('prev.slider');
    }).bind(this));

    this.$element.on('next.slider', this.next.bind(this));
    this.$document.on('click', this.options.next, (function(e) {
        this.$element.trigger('next.slider');
    }).bind(this));

    this.$element.on('update.slider', this.update.bind(this));
    this.$window.on('resize load',(function(e) {
        this.$element.trigger('update.slider');
    }).bind(this));

    this.$element.on('jump.slider', this.jump.bind(this));
    this.$document.on('click', ('.'+this.options.dotClass), (function(e) {
        var index = parseInt($(e.target).attr('data-index'));
        this.$element.trigger('jump.slider', index);
    }).bind(this));

    this.$element.on('autoplay.slider', this.autoplay.bind(this));
    this.$element.on('autoplayOn.slider', this.autoplayOn.bind(this));
    this.$element.on('autoplayOff.slider', this.autoplayOff.bind(this));
    this.$element.bind('prev.slider next.slider jump.slider', this.autoplay.bind(this));
    this.options.autoplay && this.$element.trigger('autoplayOn.slider');
};

Bring out the sledgehammer and let’s break down this bad boy…

First up, we set the slider element to respond to a custom prev.slider event. Here, prev is the event and slider is our unique namespace. Throughout this tutorial, we will use slider namespace.

this.$element.on('prev.slider', this.prev.bind(this));

Notice that we are using bind – this is needed to fix the context of this inside our handler. The bind function is pretty safe to use and is supported on all major browsers including IE 9 and above. You can also use jQuery.proxy but I will stick with bind in this tutorial.

The previous action element will trigger our custom event prev.slider on the slider

this.$document.on('click', this.options.prev, (function(e) {
    this.$element.trigger('prev.slider');
}).bind(this));

Here is the function that handles our prev.slider event

slider.prototype.prev = function(e) {
    this.index = Math.abs(this.index - 1 + this.count) % this.count;
    this.slide();
};

Simple function that finds the index of the previous slide using modular arithmetic

this.index = Math.abs(this.index - 1 + this.count) % this.count;

and slides to the new index using the slide function

slider.prototype.slide = function(index) {
    undefined == index && (index = this.index);
    var position = index * this.width * -1;
    this.$canvas.css({
        'transform' : 'translate3d(' + position + 'px, 0, 0)',
    });
    this.updateCssClass();
};

The slide function takes a single argument, an index of the target slide. If not provided, then it uses the current slide index instead.

undefined == index && (index = this.index);

It calculates the position of the target slide

var position = index * this.width * -1;

and uses CSS3 transform to slide the canvas. Note that I am not using any vendor prefixing to keep the code minimal.

this.$canvas.css({
    'transform' : 'translate3d(' + position + 'px, 0, 0)',
});

Finally, it updates CSS classes of elements where needed.

this.updateCssClass();

The updateCssClass function simply adds an active class to currently active slide and proper pagination element.

slider.prototype.updateCssClass = function() {
    this.$slides
    .removeClass('active')
    .eq(this.index)
        .addClass('active');

    this.$dots
    .find('.' + this.options.dotClass)
        .removeClass('active')
        .eq(this.index)
            .addClass('active');
}

Back to our event handler function. We do something similar for the next event like we did for the previous event.

this.$element.on('next.slider', this.next.bind(this));
this.$document.on('click', this.options.next, (function(e) {
    this.$element.trigger('next.slider');
}).bind(this));

The slider responds to the custom next.slider event

this.$element.on('next.slider', this.next.bind(this));

Here is the next event handler

slider.prototype.next = function(e) {
    this.index = (this.index + 1) % this.count;
    this.slide();
};

The only thing that differs from the previous event handler is the way we calculate the next slide index

this.index = (this.index + 1) % this.count;

We have taken care of next and previous events. Now, we make sure that our slider remains responsive when the window is resized.  This is done with the following code

this.$element.on('update.slider', this.update.bind(this));
this.$window.on('resize load',(function(e) {
    this.$element.trigger('update.slider');
}).bind(this));

Our slider will respond to update.slider custom event.

this.$element.on('update.slider', this.update.bind(this));

We trigger the update.slider custom event when the window is resized and window load is complete. Our slider will resize itself accordingly. It will also adjust the slider when all images have been loaded.

this.$window.on('resize load',(function(e) {
    this.$element.trigger('update.slider');
}).bind(this));

Let’s look at the handler function of our update.slider custom event

slider.prototype.update = function() {
    this.width = this.$element.width();
    this.$canvas.width(this.width * this.count);
    this.$slides.width(this.width);
    this.slide();
};

This function calculates the new width of the slider, then updates the width of the canvas and individual slides and finally slides the canvas back to the new position.

Remember the pagination feature we needed? Let’s make it work now. Our slider will respond to a custom jump.slider event which can be used to move back and forth between any slides.

this.$element.on('jump.slider', this.jump.bind(this));

The handler for our jump.slider event uses the following code

slider.prototype.jump = function(e, index) {
    this.index = index % this.count;
    this.slide();
}

The handler takes the index of the target slide, sets it as the current slide

this.index = index % this.count;

and slides the canvas

this.slide();

All pagination elements when clicked trigger the jump.slider event passing the value of their data attribute index.

this.$document.on('click', ('.'+this.options.dotClass), (function(e) {
    var index = parseInt($(e.target).attr('data-index'));
    this.$element.trigger('jump.slider', index);
}).bind(this));

Notice that you need to parse the value of the attribute when retrieving it using attr function or else it will be fetched as a string.

The final part of our event handler deals with the auto-play features.

this.$element.on('autoplay.slider', this.autoplay.bind(this));
this.$element.on('autoplayOn.slider', this.autoplayOn.bind(this));
this.$element.on('autoplayOff.slider', this.autoplayOff.bind(this));
this.$element.bind('prev.slider next.slider jump.slider', this.autoplay.bind(this));
this.options.autoplay && this.$element.trigger('autoplayOn.slider');

The autoplay function handles the autoplay.slider custom event triggered on the slider element.

this.$element.on('autoplay.slider', this.autoplay.bind(this));

The autoplay function code

slider.prototype.autoplay = function(argument) {
    this.autoplayClear();
    if (this.options.autoplay) {
        this.autoplayId = setTimeout((function() {
            this.$element.trigger('next.slider');
            this.$element.trigger('autoplay.slider');
        }).bind(this), this.options.autoplayTime);
    }
};

First, our autoplay function calls another function autoplayClear which has the following code

slider.prototype.autoplayClear = function() {
    this.autoplayId && clearTimeout(this.autoplayId);
}

The autoplayClear function simply checks whether there is an autoplayId variable set and if set it calls clearTimeout to clear any Timeout associated with that id.

After clearing any existing Timeout , the function checks if autoplay is enabled. If autoplay is enabled, the function sets a Timeout that uses an anonymous function to trigger next.slider and autoplay.slider events on the slider element after the autoplayTime interval.

After handling the autoplay, we register autoplayOn.slider event.

this.$element.on('autoplayOn.slider', this.autoplayOn.bind(this));

When this event is triggered on the slider element, the handler turns autoplay on.

slider.prototype.autoplayOn = function(argument){
    this.options.autoplay = true;
    this.$element.trigger('autoplay.slider');
};

Next up, an event handler for autoplayOff.slider event. Can you guess what it does?

this.$element.on('autoplayOff.slider', this.autoplayOff.bind(this));

Yep! you are right – it turns autoplay off.

slider.prototype.autoplayOff = function() {
    this.autoplayClear();
    this.options.autoplay = false;
}

Near the end of our autoplay function, we set the autoplay handler to trigger whenever any of prev.slider, next.slider or jump.slider events take place.

this.$element.bind('prev.slider next.slider jump.slider', this.autoplay.bind(this));

We do this to make sure that previous autoplay time is reset whenever the user manually navigates to a slide.

And finally, we check if autoplay was enabled when initializing the plugin. If autoplay was turned on, then we trigger the autoplayOn.slider event.

this.options.autoplay && this.$element.trigger('autoplayOn.slider');

The only thing that remains is to take care of the plugin interface. Scroll down all the way to the bottom of our code to find

$.fn[pluginName] = function(options) {
    return this.each(function() {
        !$.data(this, pluginName) && $.data(this, pluginName, new slider(this, options));
    });
};

Familiar looking jQuery plugin code that initializes the plugin and sets up a data attribute to access the plugin data.

That wraps up our plugin code.

The API

Use plugin

$('#slider').slider({
    prev: '#prev',
    next: '#next',
    dots: '#dots',
    autoplay: true,
});

Move to next slide

$('#slider').trigger('next.slider');

Move to previous slide

$('#slider').trigger('prev.slider');

Jump to a slide – index starts at 0

$('#slider').trigger('jump.slider', 2);

Start autoplay

$('#slider').trigger('autoplayOn.slider');

Stop autoplay

$('#slider').trigger('autoplayOff.slider');

Get all plugin data

$('#slider').data('slider');

Conclusion

This concludes this tutorial. The above code is licensed under MIT. Feel free to use it for any purpose you like. Hope this tutorial helped you in some way and you learned something new. Please leave your feedback in the comments section below.

Configure ETags

Next Article

How to Configure ETags to Increase your Sites Performance