Let’s Chat

Simple, Functional, Flexible: Building a Better Breed of Slider

Flying Hippo

By Flying Hippo

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.

kissing-toads-website-guide

Share Article

Close Video Player