Advertisement

Building a jQuery Image Scroller

by

In this tutorial, we're going to be building an image scroller, making use of jQuery's excellent animation features and generally having some fun with code. Image scrollers are of course nothing new; versions of them come out all the time. Many of them however are user-initiated; meaning that in order for the currently displayed content to change, the visitor must click a button or perform some other action. This scroller will be different in that it will be completely autonomous and will begin scrolling once the page loads.


The finished widget will be completely cross-browser and perform as expected in the latest versions of all of the most common browsers. We'll also build in some interaction by adding controls that allow the visitor to change the direction of the animation. We'll be working with just jQuery and a little HTML and CSS in this tutorial and should be able to run the examples without a full web server setup.


Getting Started

Let's create the underlying HTML page first of all; in a new page in your text editor add the following code:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="imageScroller.css">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>imageScroller Image Carousel</title>
  </head>
  <body>
    <div id="outerContainer">
      <div id="imageScroller">
	  <div id="viewer" class="js-disabled">
	    <a class="wrapper" href="http://www.apple.com" title="Apple"><img class="logo" id="apple" src="logos/apple.jpg" alt="Apple"></a>
	    <a class="wrapper" href="http://mozilla-europe.org/en/firefox" title="Firefox"><img class="logo" id="firefox" src="logos/firefox.jpg" alt="Firefox"></a>
	    <a class="wrapper" href="http://jquery.com" title="jQuery"><img class="logo" id="jquery" src="logos/jquery.jpg" alt="jQuery"></a>
	    <a class="wrapper" href="http://twitter.com" title="Twitter"><img class="logo" id="twitter" src="logos/twitter.jpg" alt="Twitter"></a>
	    <a class="wrapper" href="http://jqueryui.com" title="jQuery UI"><img class="logo" id="jqueryui" src="logos/jqueryui.jpg" alt="jQuery UI"></a>
	  </div>
      </div>
    </div>
    <script type="text/javascript" src="http://jqueryjs.googlecode.com/files/jquery-1.3.2.min.js"></script>
    <script type="text/javascript">
	$(function() {

      });
    </script>
  </body>
</html>

Save this as imageScroller.html inside a new folder. We link to a custom stylesheet in the head of the page, which we'll code in a little while, and we include a link to the hosted version of the latest release of jQuery at the bottom of the page. Loading scripts at the end of the body is a recognized technique for improving the performance of your page and should therefore be practiced wherever possible.

Our widget consists of a series of nested containers and a bunch of images wrapped in links. The images placed within the containers are hardcoded into the page for accessibility reasons. We won't be retrieving the images dynamically; any images placed in the widget will automatically be scrolled (provided they are wrapped in a link with the appropriate class name).

The outer-most container will be used primarily for positional and display purposes, while the next container is used to decorate the widget with a background image. The outer container is also necessary for appending the controls to so that they appear above the content correctly in IE.

The inner-most container is the element that will be used to view the images through. This element is given the class js-disabled which will be used purely for visitors that have JavaScript disabled. We'll use this class to shrink each of the images with CSS so that they are all viewable.

The images are all a uniform size, and the containers will be sized to accommodate them quite neatly. The image size is also used in the script that we'll add; I'll highlight specifically where these references occur but you should be aware that if you'd like to use images of a different size, the script and the size of the containers will need to be adjusted accordingly.


Styling the Widget

After the link to jQuery, we have a custom script element with the jQuery document.ready shortcut, waiting for us to add the code that will bring the widget to life. Before we do that however, let's just add the CSS quickly. In another new file in your text editor, add the following selectors and style rules:

/* js-disabled class - set image sizes so they all fit in the viewer */
.js-disabled img { width:100px; height:100px; display:block; float:left; margin:30px 0 0; }

#outerContainer { width:542px; height:202px; margin:auto; position:relative; }
#imageScroller { width:542px; height:202px; position:relative; background:#000000 url(images/imageScrollerBG.png) no-repeat; }
#viewer { width:522px; height:182px; overflow:hidden; margin:auto; position:relative; top:10px; }
#imageScroller a:active, #imageScroller a:visited { color:#000000; }
#imageScroller a img { border:0; }
#controls { width:534px; height:47px; background:url(images/controlsBG.png) no-repeat; position:absolute; top:4px; left:4px; z-index:10;	}
#controls a { width:37px; height:35px; position:absolute; top:3px; }
#controls a:active, #controls a:visited { color:#0d0d0d; }
#title { color:#ffffff; font-family:arial; font-size:100%; font-weight:bold; width:100%; text-align:center; margin-top:10px; }
#rtl { background:url(images/rtl.png) no-repeat; left:100px; }
#rtl:hover { background:url(images/rtl_over.png) no-repeat; left:99px; }
#ltr { background:url(images/ltr.png) no-repeat; right:100px; }
#ltr:hover { background:url(images/ltr_over.png) no-repeat; }

If JavaScript is disabled, and while the page is loading, all of the images will be viewable

Save this as imageScroller.css in the same folder as the web page. First we have the class selector that targets our js-disabled class; with these rules we simply size the images so that they are small enough to stack up next to each other along the width of the widget. If JavaScript is disabled, and while the page is loading, all of the images will be viewable – a very quick and easy fallback, but one that isn't necessarily fool-proof and certainly isn't complete Progressive Enhancement. The values specified for the width and height will need to vary depending on the number of images in the viewer.

Following this we have the selectors and rules that style the widget and make it function correctly. Most of the code here is purely for display purposes, background-images, colors, etc. An important rule, which the implementation relies upon to function correctly, is the setting of overflow:hidden on the inner viewer container. This will hide the images that have yet to be shown and the images that have already passed though the viewer. At this stage when we run the page, we should see something like this:

Finished Product

Some of the CSS we'll set in the JavaScript in just a moment, and some of the elements that we're targeting in the CSS don't exist yet, but this is everything that needs to go into the CSS file.


Bringing the Widget to Life

In the final stage of this tutorial we'll add the jQuery-flavored JavaScript that will make the widget work and create the behavior that we desire. Within the empty anonymous function at the bottom of the HTML page add the following code:

//remove js-disabled class
$("#viewer").removeClass("js-disabled");
			
//create new container for images
$("<div>").attr("id", "container").css({ 
  position:"absolute"
}).width($(".wrapper").length * 170).height(170).appendTo("div#viewer");
			  	
//add images to container
$(".wrapper").each(function() {
  $(this).appendTo("div#container");
});
				
//work out duration of anim based on number of images (1 second for each image)
var duration = $(".wrapper").length * 1000;
				
//store speed for later
var speed = (parseInt($("div#container").width()) + parseInt($("div#viewer").width())) / duration;
								
//set direction
var direction = "rtl";
				
//set initial position and class based on direction
(direction == "rtl") ? $("div#container").css("left", $("div#viewer").width()).addClass("rtl") : $("div#container").css("left", 0 - $("div#container").width()).addClass("ltr") ;

First of all we remove the js-disabled class from the viewer container. Next we create a new container to hold all of the images that are found within the widget. The main reason for this is so that instead of animating each image individually, resulting in a potentially large number of animations running simultaneously, we only have to animate one element – the container that we're creating now.

The width of the new container is set to the number of images multiplied by the width of each image, which in this example is 170 pixels. This is one of the bits of code that I said earlier I would specifically mention, and is something that will need to be changed if we decide to use images of a different size. The height of the container is also specifically set to the height of each image.

It is useful later on in the script to know certain things about the nature of the animation, such as its speed, the duration it will last and the direction of travel, so we next set a series of variables to store this information in. The duration will equate to exactly one second per image, and is based again on the number of images found in the widget.

The speed is easy to work out, being of course the distance of travel divided by the duration of travel. For reference, in this example the exact speed of the animation will be 0.274 pixels-per-millisecond. The final variable, direction, is a simple string denoting that the animation will proceed from right-to-left, although we could easily change this to ltr if we wished.

Finally, we set the starting position of the new container; as the animation is currently set to rtl, we need to position the new image container so that its left edge is set to the right edge of the viewer. If we set the animation to ltr however, the element's right edge will be aligned with the container's left edge. We determine the direction using the JavaScript ternary conditional. As well as its position, we also give the new container a class name matching its direction, which we can test for at different points in the script.

Next we'll need to define a new function to initiate and perpetuate the animation. There are several different times during the normal execution of the script that we'll need to begin animating, so wrapping this functionality in a function that we can call when we need helps to reduce the amount of code. Add the following code:

//animator function
var animator = function(el, time, dir) {
				 
  //which direction to scroll
  if(dir == "rtl") {
					  
    //add direction class
    el.removeClass("ltr").addClass("rtl");
					 		
    //animate the el
    el.animate({ left:"-" + el.width() + "px" }, time, "linear", function() {
										
	//reset container position
	$(this).css({ left:$("div#imageScroller").width(), right:"" });
							
	//restart animation
	animator($(this), duration, "rtl");
							
	//hide controls if visible
	($("div#controls").length > 0) ? $("div#controls").slideUp("slow").remove() : null ;			
							
    });
  } else {
					
    //add direction class
    el.removeClass("rtl").addClass("ltr");
					
    //animate the el
    el.animate({ left:$("div#viewer").width() + "px" }, time, "linear", function() {
											
      //reset container position
      $(this).css({ left:0 - $("div#container").width() });
							
      //restart animation
      animator($(this), duration, "ltr");
							
      //hide controls if visible
      ($("div#controls").length > 0) ? $("div#controls").slideUp("slow").remove() : null ;			
    });
  }
}

The animator function accepts three arguments; the element to animate, the length of time that the animation should run for, and the direction in which the element should be animated. The function is broken up into two distinct blocks, one for rtl animation and the other for ltr.

Within each block of the conditional we update the class name of the image container to reflect the current direction just in case the direction has changed (this is one of the visitor initiated interactions).

We then define the animation, moving the image container plus for ltr or minus for rtl the width of the image container, giving it the impression of sliding across the viewer. Unfortunately we can't use the built in slow, normal, or fast animations, because even the slow setting limits the animation to a total run time of only 600 milliseconds, which is way too fast for even the small number of images we are using in this example.

We specify the string linear as the third argument of the animate method which is the easing function to use and sets the animation to proceed at a uniform speed from start to finish; if we didn't set this, the animation would noticeably speed up and slow down at the beginning and end of the animation respectively.

Finally we add an anonymous callback function which will be executed as soon as the animation ends; within this callback function, we return the image container to its starting position, recursively call the animator function again passing in the correct settings depending on which branch of the conditional is being executed, and hide the control panel if it is visible. We haven't added the code that will create the control panel yet, but we still need to add this code here for when we have.

In order to start the animation when the page has loads we now need to call the function that we've just defined; add the following function call:

//start anim
animator($("div#container"), duration, direction);

All we do is call the function passing in the element to animate and the variables we set in the first section of code. If we run the page now, we should find that the animation starts as soon as the page has loaded and continues indefinitely, as shown (kind of) in the following screenshot:

Finished Product

Adding Some Interaction

We're now at the stage where we have the core functionality of the widget and can start adding the extra interactivity that will make it engaging. After the call to the animator function add the following code:

//pause on mouseover
$("a.wrapper").live("mouseover", function() {
				  
  //stop anim
  $("div#container").stop(true);
					
  //show controls
($("div#controls").length == 0) ? $("<div>").attr("id", "controls").appendTo("div#outerContainer").css({ opacity:0.7 }).slideDown("slow") : null ;
($("a#rtl").length == 0) ? $("<a>").attr({ id:"rtl", href:"#", title:"rtl" }).appendTo("#controls") : null ;
($("a#ltr").length == 0) ? $("<a>").attr({ id:"ltr", href:"#", title:"ltr" }).appendTo("#controls") : null ;
					
  //variable to hold trigger element
  var title = $(this).attr("title");
					
  //add p if doesn't exist, update it if it does
  ($("p#title").length == 0) ? $("<p>").attr("id", "title").text(title).appendTo("div#controls") : $("p#title").text(title) ;
});

As the comment indicates, this event handler will stop the animation when the visitor hovers the pointer on one of the images within the widget.

We use the live jQuery (new to 1.3!) method to attach the handler to the elements and specify an anonymous function to be executed when the event occurs.

Within this function we first stop the animation using the jQuery stop method, passing in a true Boolean value as an argument. This argument will cancel the animation queue if it exists; it shouldn't do, as there should only ever be one animation at any one time, but it's useful to use this argument just in case.

We check whether the control panel already exists and provided it doesn't we create a new div element, give it an id so that it picks up our style rules and appends it to the outer container. We then use jQuery's css method to set the opacity in a cross-browser fashion to avoid having to target different browsers with our CSS, and slide the controls down into place.

We also create some links and append them to the control panel; these links will act as buttons which allow the visitor to change the direction that the images are moving. We'll add handlers for these buttons in just a moment. Finally, we obtain the contents of the title attribute of the wrapper link that triggered the mouseover event and create a new paragraph element with its inner text set to the title. We rely heavily on the JavaScript ternary conditional shortcut in this section of code as it provides an excellent mechanism for only creating and appending elements if they don't already exist.

You may also have noticed that we set a variable to hold the contents of the current trigger's title attribute, you may be wondering why we don't use the following code instead:

//add p if doesn't exist, update it if it does
($("p#title").length == 0) ? $("<p>").attr("id", "title").text($(this).attr("title")).appendTo("div#controls") : $("p#title").text(title) ;

The reason for this is so that there is no ambiguity as to what $(this) refers to. Using the above code does work, but it throws errors, which while non-fatal, still aren't that reassuring for potential users of the widget. Using the variable simply ensures these errors are avoided. The control panel, when visible, appears as in the following screenshot:

Finished Product

Following the mouseover the animation will be stopped; we can start it again easily using a mouseout event handler, which we should add next:

//restart on mouseout
$("a.wrapper").live("mouseout", function(e) {
				  
  //hide controls if not hovering on them
  (e.relatedTarget == null) ? null : (e.relatedTarget.id != "controls") ? $("div#controls").slideUp("slow").remove() : null ;
					
  //work out total travel distance
  var totalDistance = parseInt($("div#container").width()) + parseInt($("div#viewer").width());
														
  //work out distance left to travel
  var distanceLeft = ($("div#container").hasClass("ltr")) ? totalDistance - (parseInt($("div#container").css("left")) + parseInt($("div#container").width())) : totalDistance - (parseInt($("div#viewer").width()) - (parseInt($("div#container").css("left")))) ;
					
  //new duration is distance left / speed)
  var newDuration = distanceLeft / speed;
				
  //restart anim
  animator($("div#container"), newDuration, $("div#container").attr("class"));

});

Again we use jQuery's live method, but this time, we also pass the raw event object into our anonymous callback function. We make use of this object straight away to see whether the pointer has moved onto the control panel. If it hasn't, we hide the controls, but if it has, we do nothing and proceed with restarting the animation. Notice how we use a nested ternary that is equivalent to an if else conditional.

The main purpose of the anonymous function is to restart the animation, but before we can do that we need to work out the duration of the animation; we can't hardcode the value because the image container will have moved. The initial duration was set to 1 second for each image, in this example 5 seconds. If there is only one image left visible in the viewer and we set the animation to 5 seconds again, the animation will proceed markedly slower.

We first work out what the total distance is that the image container travels in a full animation. We then work out how much of the full distance is still to be traveled. We'll need to do a different calculation depending on whether the animation is happening from left to right or the opposite, so we again make use of the ternary conditional.

If the animation is occurring from left to right, the distance left to travel is the left style attribute of the image container (obtained using the css jQuery method) added to the width of the image container, subtracted from the total distance. If the image container is moving from right to left however, the distance left to travel is the width of the image container minus the left style attribute, subtracted from the total distance. The width and css jQuery methods return string values, so we use JavaScript's parseInt function to convert these to numerical values.

The new duration of the animation is then calculated by dividing the distance left to travel by the speed that we worked out right at the start of the code. Once we have this figure, we can then call the animator function again passing in the required parameters, making the animation start up again from where it stopped, in the same direction of travel.


Changing Direction

For the final part of our script we can add the handlers for the links in the control panel used to change the direction of the animation. Directly after the code we just added, enter the following code:

//handler for ltr button
$("#ltr").live("click", function() {
				 					
  //stop anim
  $("div#container").stop(true);
				
  //swap class names
  $("div#container").removeClass("rtl").addClass("ltr");
										
  //work out total travel distance
  var totalDistance = parseInt($("div#container").width()) + parseInt($("div#viewer").width());
					
   //work out remaining distance
  var distanceLeft = totalDistance - (parseInt($("div#container").css("left")) + parseInt($("div#container").width()));
					
  //new duration is distance left / speed)
  var newDuration = distanceLeft / speed;
					
  //restart anim
  animator($("div#container"), newDuration, "ltr");
});

This function, triggered when the left to right button is clicked, is relatively simple and contains code very similar to what we have already used; we first stop the current animation (it will have resumed when the visitor moves the pointer over the control panel), and then swap the class name so that it matches the new direction of travel. We then work out the new duration of the animation in the same way that we did earlier, before finally calling our animator function once again. This is just the handler for the ltr button; the handler for the rtl button is almost identical, but uses the correct calculation for the opposite direction of travel:

//handler for rtl button
$("#rtl").live("click", function() {
										
  //stop anim
  $("div#container").stop(true);
					
  //swap class names
  $("div#container").removeClass("ltr").addClass("rtl");
					
  //work out total travel distance
  var totalDistance = parseInt($("div#container").width()) + parseInt($("div#viewer").width());

  //work out remaining distance
  var distanceLeft = totalDistance - (parseInt($("div#viewer").width()) - (parseInt($("div#container").css("left"))));
					
  //new duration is distance left / speed)
  var newDuration = distanceLeft / speed;
				
  //restart anim
  animator($("div#container"), newDuration, "rtl");
});

This is now all of the code we need to write, if you run the page in a browser at this point, you should find that the widget works as intended.


Summary

In this tutorial we've created a fun and interactive widget for displaying a series of images and could be used to display logos of the manufacturers of products that you sell, or the logos of software that you recommend, or anything else that you like. We focused mainly on the animation and interaction aspects of the widget, but also considered things such as providing a basic fallback in case JavaScript is disabled in the browser.

Advertisement