Simple, Functional, Flexible: Building a Better Breed of Slider
It seems like jQuery sliders are a dime a dozen these days. Yet, most aren’t easy to use out of the box.
Maybe the “Super Simple jQuery Slider” is just a myth. But, I don’t think so. I think we can do better. I think we can create a slider that targets an unordered list of images, with tasteful crossfades, automatic transitions that pause on hover, controls, and infinite looping. (Even with all of that functionality, we’ll still call that a “simple slider”.)
The truth is, simple and easy are almost never the same thing (cue the Jony Ive video, but it’s true). We want something that works on every device, that never confuses the user, and gets them where they need to go. But, it’s not impossible. And, I think we’ve come up with a pretty nice solution.
Ready? Let’s get started!
The Markup
First, let’s look at the base code:
<!DOCTYPE html> <html> <head> <title>Slider</title> <link rel="stylesheet" href="style.css" /> </head> <body> <div class="fh-carousel-wrapper"> <ul class="fh-carousel"> <li> <a href="//flyinghippo.com"> <img src="assets/bg1.png" /> </a> </li> <li> <img src="assets/bg2.png" /> </li> <li> <a href="http://google.com"> <img src="assets/bg3.png" /> </a> </li> </ul> <div class="fh-carousel-controls"> <a href="#" class="prev"></a> <a href="#" class="next"></a> </div> </div> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script> <script src="js/imageload.js"></script> <script src="js/slider.js"></script> </body> </html>
As you’ll see, some of our images are wrapped in links and some aren’t. Most people will want to use links, but it’s good to make them optional. We want our slider to work in as many applications as possible, so flexibility is key. Other than that, the HTML is pretty basic. We load our stylesheet at the top and our JavaScript at the bottom. Our images are in an unordered list, and the entire slider is wrapped in a div with a class of fh-carousel-wrapper.
The Styling
The styling code is a little more complex to make sure it works across devices and for different screen sizes and resolutions. The website A List Apart has a detailed article on something called intrinsic ratios that we can use for reference. The article talks about video resizing, but we can use it here as well. First, let’s see the code:
.fh-carousel-wrapper { height: auto; position: relative; } .fh-carousel { line-height: 0; list-style-type: none; position: relative; } .fh-carousel > * { display: block; left: 0; line-height: 0; position: absolute; top: 0; width: 100%; } .fh-carousel img { display: block; height: auto; width: 100%; } .fh-carousel-controls { background-color: #fff; border-radius: 5px 5px 0 0; bottom: 0; left: 50%; margin: 0 0 0 -48px; position: absolute; z-index: 101; } .fh-carousel-controls a { display: block; float: left; height: 48px; padding: 8px 12px; width: 48px; } .fh-carousel-controls .prev { background: url( images/prev.png ) 24px 16px no-repeat; } .fh-carousel-controls .next { background: url( images/next.png ) 8px 16px no-repeat; }
The .fh-carousel-wrapper property is set to “position: relative” to keep all of the children elements in line. The .fh-carousel also has the same positioning, for its child elements (along with a line-height of 0 to prevent extra padding at the bottom of the slider).
In order to make linking optional, we target any immediate children, using the .fh-carousel > * selector. In this property, we set the position to absolute (along with the top and left position), and the width to 100%. We then target image elements separately, to make sure they also fill the width and are set to display as block elements. Moving on to the navigation, we set this to be at the bottom, center of the slider, and add some icons.
The jQuery
Here’s where things get interesting. Let’s post the code, and go over it line by line:
jQuery( document ).ready( function( $ ) { // Set id, item position, item count, auto_slide interval, interval time, and autoplay setting var position = 0; var count = $( ".fh-carousel > *" ).length - 1; var autoslide; // Calculate Images function calculateImages() { // Calculate Image Height var imageload = $( ".fh-carousel" ).find( 'img' ).first().imagesLoaded(); imageload.done( function( $image ) { var height = ( $image.height() / $image.width() ) * 100; $image.closest( '.fh-carousel' ).children( '*' ).first().css( 'position', 'absolute' ); $image.closest( '.fh-carousel' ).css( 'padding', "0 0 " + height + "%" ); }); // Set z-indexes in reverse so first image shows first var carousel_zindex = $( ".fh-carousel" ).children( '*' ).length; $( ".fh-carousel" ).children( '*' ).each( function() { $( this ).css( 'z-index', carousel_zindex ); carousel_zindex--; }); } // Next Slide function doNextSlide() { if( position === count ) { position = 0; $( ".fh-carousel > *" ).first().fadeIn( 300, function() { $( ".fh-carousel > *" ).show(); }); } else { $( ".fh-carousel > *" ).eq( position ).fadeOut( 300 ); position++; } } // Previous Slide function doPreviousSlide() { if( position === 0 ) { position = count; $( ".fh-carousel > *" ).not( $( ".fh-carousel > *" ).last() ).not( $( ".fh-carousel > *" ).first() ).hide(); $( ".fh-carousel > *" ).first().fadeOut( 300 ); } else { position--; $( ".fh-carousel > *" ).eq( position ).fadeIn( 300 ); } } calculateImages(); autoslide = setInterval( function() { doNextSlide(); }, 4000 ); // Control Slideshow $( ".fh-carousel-controls a" ).on( 'click', function( e ) { if( $( this ).hasClass( 'next' ) ) { doNextSlide(); } if( $( this ).hasClass( 'prev' ) ) { doPreviousSlide(); } e.preventDefault(); }); // Pause auto slide functionality on hover $( ".fh-carousel-wrapper" ).on( 'mouseenter', function() { // Pause auto slide functionality clearInterval( autoslide ); }).on( 'mouseleave', function() { // Restart auto slide functionality autoslide = setInterval( function() { doNextSlide( n ); }, 4000 ); }); });
First things first: We need to make sure to include the jQuery imageLoad plugin (Note: As of writing, latest version of this plugin seems to have an error, please see the end of the post for the plugin code we used). This will make sure nothing is calculated until the images are loaded (for example, the image height).
Line 1 of the code is a standard check to make sure the document is ready. Then, lines 4-6 set the initial position to zero, get the number of slides, and set an empty variable called autoslide, which will be used to set the interval for autoplaying the slides. We then have three functions: calculateImages, doNextSlide, and doPreviousSlide that construct the slider and give us manual control between each image.
The calculateImages function sets the bottom padding, used for the height (see the A List Apart article and the styling section of this tutorial). This function also makes sure the first slide in the HTML code is shown on top of the others by setting the z-indexes in reverse (so the first image has a z-index of 3, the second 2, and the third 1).
For navigating our slides, we can either go forward or backward. The doNextSlide function goes — you guessed it — the next slide in our stack (forward). There’s also code here that checks to see if our current position is equal to the amount of slides we have, and, if so, shows the first slide and resets the position variable. Otherwise, it simply fades out the current slide and increases the position count.
When navigating backward in the slide stack, we will use the doPreviousSlide function. If the position variable equals zero, we will show the last slide. To do this, we need to hide all of the slides on top, but we don’t want to fade them out, as multiple slides will show. To fix this, we hide all slides except for the first and last. Then, fade out the first slide. If the position variable does not equal zero, we fade in the previous slide and lower the position count (we actually lower this first, so that we don’t have to do math to find the previous slide).
After our function declarations, we call the calculateImages function on line 67 and set the slider to autoplay using the JavaScript setInterval function (the 4000 integer is in milliseconds — and sets the interval to fire every four seconds). Lines 75-87 fire the doNextSlide and doPreviousSlide functions depending on which link was clicked.
Finally, we have some logic to pause the autoplay feature if the user is hovering over the slider. We simply check if the mouse has entered the slider area, and then clear the interval. If the mouse leaves, we then reset the interval to start again.
Here’s the version of the jQuery imageLoad plugin used for this slider:
/*! * jQuery imagesLoaded plugin v2.0.1 * http://github.com/desandro/imagesloaded * * MIT License. by Paul Irish et al. */ /*jshint curly: true, eqeqeq: true, noempty: true, strict: true, undef: true, browser: true */ /*global jQuery: false */ ;(function($, undefined) { 'use strict'; // blank image data-uri bypasses webkit log warning (thx doug jones) var BLANK = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw=='; $.fn.imagesLoaded = function( callback ) { var $this = this, deferred = $.isFunction($.Deferred) ? $.Deferred() : 0, hasNotify = $.isFunction(deferred.notify), $images = $this.find('img').add( $this.filter('img') ), loaded = [], proper = [], broken = []; function doneLoading() { var $proper = $(proper), $broken = $(broken); if ( deferred ) { if ( broken.length ) { deferred.reject( $images, $proper, $broken ); } else { deferred.resolve( $images ); } } if ( $.isFunction( callback ) ) { callback.call( $this, $images, $proper, $broken ); } } function imgLoaded( img, isBroken ) { // don't proceed if BLANK image, or image is already loaded if ( img.src === BLANK || $.inArray( img, loaded ) !== -1 ) { return; } // store element in loaded images array loaded.push( img ); // keep track of broken and properly loaded images if ( isBroken ) { broken.push( img ); } else { proper.push( img ); } // cache image and its state for future calls $.data( img, 'imagesLoaded', { isBroken: isBroken, src: img.src } ); // trigger deferred progress method if present if ( hasNotify ) { deferred.notifyWith( $(img), [ isBroken, $images, $(proper), $(broken) ] ); } // call doneLoading and clean listeners if all images are loaded if ( $images.length === loaded.length ){ setTimeout( doneLoading ); $images.unbind( '.imagesLoaded' ); } } // if no images, trigger immediately if ( !$images.length ) { doneLoading(); } else { $images.bind( 'load.imagesLoaded error.imagesLoaded', function( event ){ // trigger imgLoaded imgLoaded( event.target, event.type === 'error' ); }).each( function( i, el ) { var src = el.src; // find out if this image has been already checked for status // if it was, and src has not changed, call imgLoaded on it var cached = $.data( el, 'imagesLoaded' ); if ( cached && cached.src === src ) { imgLoaded( el, cached.isBroken ); return; } // if complete is true and browser supports natural sizes, try // to check for image status manually if ( el.complete && el.naturalWidth !== undefined ) { imgLoaded( el, el.naturalWidth === 0 || el.naturalHeight === 0 ); return; } // cached images don't fire load sometimes, so we reset src, but only when // dealing with IE, or image is complete (loaded) and failed manual check // webkit hack from http://groups.google.com/group/jquery-dev/browse_thread/thread/eee6ab7b2da50e1f if ( el.readyState || el.complete ) { el.src = BLANK; el.src = src; } }); } return deferred ? deferred.promise( $this ) : $this; }; })(jQuery);
Conclusion
With only a relatively small amount of code, we have built a fully-functional slider that works with a variable amount of images, and which can be re-sized to work on any device. The cool thing is that we were able to do this without sacrificing our high-quality crossfade animation.
Is this the end-all, be-all of content sliders? Probably not. Are sliders the right choice for every application? Definitely not. But, when a slider is the right solution, a simple, light-weight, and flexible solution is what you need. This is that slider.