Search

jQuery Infinite Carousel

Posted on 11th June 2009 — With jQuery for Designer’s redesign I decided to add a scrolling carousel that worked in the same way the carousel worked on the Apple Mac ads page. This tutorial will walk through the fundamentals of the effect and how to recreate it.

Watch

Watch jQuery Infinite Carousel screencast (Alternative flash version)

QuickTime version is approximately 70Mb, flash version is streaming.

View the demo used in the screencast

Example of Apple's get a mac carousel

Understanding the Effect

As with any complicated effect, I always start by looking at the DOM in Firebug and interact with the effect, watching for what changes take effect in the DOM.

What I noticed was the (rendered) DOM structure for the list of images was different to the delivered DOM (i.e. the original markup sent). This means that there’s some significant initialisation code running. This isn’t unexpected for an Apple effect, but there were big clues in to how to create this effect in the difference.

Most importantly the list of images (in a <ul> element) had additional <li> elements at the start of the list and at the end of the list. There were two types: clone and empty.

The second big clue was the number of cloned elements on the end of the list: it matched the number of visible items in the carousel.

In the diagram below I’ve tried to outline what’s cloned in the list, and show what’s visible. You can see how the list shows 3 items at once, but there’s 5 items in total. So the first cloned section (at the start of the list) contains two clones and a single empty clone. The end of the list has 3 clones - which represents real items 1-3:

Diagram of DOM to Apple's carousel

The Trick

Aside from building the DOM with the cloned nodes, the real trick is in the final panel of list items appearing. Because we’ve cloned the start of the list to the end of the list, when we scroll off the last items, it looks like we’ve looped back round. Once the animation completes, we change the scrollLeft position back to the real first items.

This scrollLeft isn’t visible to the user since the overflow is hidden, and so it creates the effect of being infinitely sliding from left or right (since it also works the other way around).

Markup

Remembering that we want the effect to work without JavaScript turned on, we’re going to use a pattern that we use fairly often for effects that scroll: overflows.

The default will be the overflow is visible, and we’ll use JavaScript to change the element to overflow: hidden.

<div class="infiniteCarousel">
  <div class="wrapper">
    <ul>
      <li>...</li>
      <li>...</li>
      <li>...</li>
    </ul>        
  </div>
</div>

The CSS has been marked up in a way that the overflow is visible to start off with, and that the individual <li> elements are the same fixed width. This is necessary to ensure that we always show complete images in the visible part of the carousel.

jQuery

Replicating the Effect

This is a reasonably advanced effect so we need to plan carefully what we’re trying to achieve:

  1. Markup that works without JavaScript (done!)
  2. Initialisation & capture the height and width of the first carousel item (required for calculations)
  3. Create empty list items to pad to fill every page
  4. Generate cloned items on the start and end of the list
  5. Adjust the scroll left position to show the real first item
  6. Create goto method that supports forward and backwards
  7. Generate the next/prev links & bind goto calls

The completed example is also available if you want to skip through each step.

Initialisation

We’re going to write the code as a plugin so we can release it out to the wild at some later point. So this follows the typical plugin pattern:

(function ($) {
  $.fn.myPlugin = function () {
    return this.each(function () {
      // plugin code
    };
  };
})(jQuery);

This step is used to initialise the plugin and capture all the variables we need to create this effect. This includes:

  1. jQuery instance of the overflow element
  2. Hiding the overflow
  3. jQuery instance of the list
  4. jQuery instance of the list items
  5. The first item and it’s width
  6. Number of visible items we can fit in the carousel (rounded up via Math.ceil)
  7. Current page (defaults to 1) and the number of pages (rounded up)
var $wrapper = $('> div', this).css('overflow', 'hidden'),
  $slider = $wrapper.find('> ul'),
  $items = $slider.find('> li'),
  $single = $items.filter(':first'),
  
  // outerWidth: width + padding (doesn't include margin)
  singleWidth = $single.outerWidth(), 
  
  // note: doesn't include padding or border
  visible = Math.ceil($wrapper.innerWidth() / singleWidth), 
  currentPage = 1,
  pages = Math.ceil($items.length / visible);

Padding with Empty Items

During the screencast I said that we’re making use of the repeat function from another tutorial - however that was wrong! I must have used it for something else! None the less, here is the repeat function, which we put outside of the plugin since it’s a utility function:

function repeat(str, n) {
  return new Array( n + 1 ).join( str );
}

The repeat function simple uses and array hack to repeat any string n times.

When we create the empty padding, we need to use modulus to work out how many new empty items we need to fill every page of our carousel with an item:

if (($items.length % visible) != 0) {
  $slider.append(repeat('<li class="empty" />', visible - ($items.length % visible)));
  $items = $slider.find('> li');
}

Notice that we reinitialise the $items variable. This is to ensure it contains all of the items, including our newly created empty items.

Generating Clones

As per the diagram in the introduction, we need to clone the last page of items to the start of the list, and the first page of items to the end of the list. These will be the items that we slide on to before we flip the scrollLeft position (as per the trick above).

$items.filter(':first').before($items.slice(-visible).clone().addClass('cloned'));
$items.filter(':last').after($items.slice(0, visible).clone().addClass('cloned'));
$items = $slider.find('> li'); // reselect

We’re making using the slice method that jQuery has. Notice that we first use it with a negative number:

$items.slice(-visible)

This means to take the last n items off the end of $items. Otherwise we have to pass the start and end point, so the second instance we’re saying from the start, 0, and the third as the method is zero-based indices.

Reset Scroll Left

Since we’ve created the clones at the start of the list, we need to shift the scroll left back to the real element, otherwise it would look like the carousel is sitting on the last page.

$wrapper.scrollLeft(singleWidth * visible);

Go To Method

The gotoPage method will handle going forward and backwards and handle jumping several pages at a time.

We’re going to use the animate method and only animate the scrollLeft so that it smoothly scrolls. We could add custom easin if we wanted at a later date.

We need to establish the following:

  1. Direction we’re going to slide
  2. Number of pages to slide
  3. The relative scroll left value

Once the animation has completed, we also need to maintain the currentPage variable we created on initialisation.

If the page number is greater than the number of pages (i.e. we’re on our cloned first page), then reset the scroll left to the real first page, and set the page number to 1.

Equally if we have hit the beginning of the list (i.e. we’re showing the cloned last page), then we reset the scroll left to the real last page, and set the page number to the total number of pages.

function gotoPage(page) {
  var dir = page < currentPage ? -1 : 1,
    n = Math.abs(currentPage - page),
    left = singleWidth * dir * visible * n;
  
  $wrapper.filter(':not(:animated)').animate({
    scrollLeft : '+=' + left
  }, 500, function () {
    if (page == 0) {
      $wrapper.scrollLeft(singleWidth * visible * pages);
      page = pages;
    } else if (page > pages) {
      $wrapper.scrollLeft(singleWidth * visible);
      page = 1;
    } 

    currentPage = page;
  });                
  
  return false;
}

Navigation buttons

Finally we need to add the buttons or links to page through the carousel and bind the gotoPage call.

We’re also going to create a custom goto event that allows us to also jump to any page:

$wrapper.after('<a class="arrow back">&lt;</a><a class="arrow forward">&gt;</a>');

$('a.back', this).click(function () {
    return gotoPage(currentPage - 1);                
});

$('a.forward', this).click(function () {
    return gotoPage(currentPage + 1);
});

// this currently refers to the element the plugin was bound to
$(this).bind('goto', function (event, page) {
    gotoPage(page);
});

Demo

That’s it. It’s a fairly complicated plugin as J4D demos go. There is also the jCarousel plugin that you could use (though I’m not sure if it has infinite scrolling) - but obviously the idea here is to show you how it all comes together.

Check out the demo, view the source, have a play!

Related posts

Demo

If you find this demo doesn't work as expected, it's possibly due to the demo running from within an iframe. Try running the demo in it's own window.

Source Code

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<title>Infinite Carousel</title>
<style type="text/css" media="screen">
<!--
body { font: 1em "Trebuchet MS", verdana, arial, sans-serif; font-size: 100%; }
input, textarea { font-family: Arial; font-size: 125%; padding: 7px; }
label { display: block; } 

.infiniteCarousel {
  width: 395px;
  position: relative;
}

.infiniteCarousel .wrapper {
  width: 315px; /* .infiniteCarousel width - (.wrapper margin-left + .wrapper margin-right) */
  overflow: auto;
  min-height: 10em;
  margin: 0 40px;
  position: absolute;
  top: 0;
}

.infiniteCarousel ul a img {
  border: 5px solid #000;
  -moz-border-radius: 5px;
  -webkit-border-radius: 5px;
}

.infiniteCarousel .wrapper ul {
  width: 9999px;
  list-style-image:none;
  list-style-position:outside;
  list-style-type:none;
  margin:0;
  padding:0;
  position: absolute;
  top: 0;
}

.infiniteCarousel ul li {
  display:block;
  float:left;
  padding: 10px;
  height: 85px;
  width: 85px;
}

.infiniteCarousel ul li a img {
  display:block;
}

.infiniteCarousel .arrow {
  display: block;
  height: 36px;
  width: 37px;
  background: url(images/arrow.png) no-repeat 0 0;
  text-indent: -999px;
  position: absolute;
  top: 37px;
  cursor: pointer;
}

.infiniteCarousel .forward {
  background-position: 0 0;
  right: 0;
}

.infiniteCarousel .back {
  background-position: 0 -72px;
  left: 0;
}

.infiniteCarousel .forward:hover {
  background-position: 0 -36px;
}

.infiniteCarousel .back:hover {
  background-position: 0 -108px;
}



-->
</style>

<script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>


<script type="text/javascript">

$.fn.infiniteCarousel = function () {

    function repeat(str, num) {
        return new Array( num + 1 ).join( str );
    }
  
    return this.each(function () {
        var $wrapper = $('> div', this).css('overflow', 'hidden'),
            $slider = $wrapper.find('> ul'),
            $items = $slider.find('> li'),
            $single = $items.filter(':first'),
            
            singleWidth = $single.outerWidth(), 
            visible = Math.ceil($wrapper.innerWidth() / singleWidth), // note: doesn't include padding or border
            currentPage = 1,
            pages = Math.ceil($items.length / visible);            


        // 1. Pad so that 'visible' number will always be seen, otherwise create empty items
        if (($items.length % visible) != 0) {
            $slider.append(repeat('<li class="empty" />', visible - ($items.length % visible)));
            $items = $slider.find('> li');
        }

        // 2. Top and tail the list with 'visible' number of items, top has the last section, and tail has the first
        $items.filter(':first').before($items.slice(- visible).clone().addClass('cloned'));
        $items.filter(':last').after($items.slice(0, visible).clone().addClass('cloned'));
        $items = $slider.find('> li'); // reselect
        
        // 3. Set the left position to the first 'real' item
        $wrapper.scrollLeft(singleWidth * visible);
        
        // 4. paging function
        function gotoPage(page) {
            var dir = page < currentPage ? -1 : 1,
                n = Math.abs(currentPage - page),
                left = singleWidth * dir * visible * n;
            
            $wrapper.filter(':not(:animated)').animate({
                scrollLeft : '+=' + left
            }, 500, function () {
                if (page == 0) {
                    $wrapper.scrollLeft(singleWidth * visible * pages);
                    page = pages;
                } else if (page > pages) {
                    $wrapper.scrollLeft(singleWidth * visible);
                    // reset back to start position
                    page = 1;
                } 

                currentPage = page;
            });                
            
            return false;
        }
        
        $wrapper.after('<a class="arrow back">&lt;</a><a class="arrow forward">&gt;</a>');
        
        // 5. Bind to the forward and back buttons
        $('a.back', this).click(function () {
            return gotoPage(currentPage - 1);                
        });
        
        $('a.forward', this).click(function () {
            return gotoPage(currentPage + 1);
        });
        
        // create a public interface to move to a specific page
        $(this).bind('goto', function (event, page) {
            gotoPage(page);
        });
    });  
};

$(document).ready(function () {
  $('.infiniteCarousel').infiniteCarousel();
});
</script>
</head>
<body>
    <h1>Infinite Carousel</h1>
    
    <div class="infiniteCarousel">
      <div class="wrapper">
        <ul>
          <li><a href="http://www.flickr.com/photos/remysharp/3047035327/" title="Tall Glow"><img src="http://farm4.static.flickr.com/3011/3047035327_ca12fb2397_s.jpg" height="75" width="75" alt="Tall Glow" /></a></li>
          <li><a href="http://www.flickr.com/photos/remysharp/3047872076/" title="Wet Cab"><img src="http://farm4.static.flickr.com/3184/3047872076_61a511a04b_s.jpg" height="75" width="75" alt="Wet Cab" /></a></li>
          <li><a href="http://www.flickr.com/photos/remysharp/3047871878/" title="Rockefella"><img src="http://farm4.static.flickr.com/3048/3047871878_84bfacbd35_s.jpg" height="75" width="75" alt="Rockefella" /></a></li>
          <li><a href="http://www.flickr.com/photos/remysharp/3047034929/" title="Chrysler Reflect"><img src="http://farm4.static.flickr.com/3220/3047034929_97eaf50ea3_s.jpg" height="75" width="75" alt="Chrysler Reflect" /></a></li>
          <li><a href="http://www.flickr.com/photos/remysharp/3047871624/" title="Chrysler Up"><img src="http://farm4.static.flickr.com/3164/3047871624_2cacca4684_s.jpg" height="75" width="75" alt="Chrysler Up" /></a></li>
          <li><a href="http://www.flickr.com/photos/remysharp/3047034661/" title="Time Square Awe"><img src="http://farm4.static.flickr.com/3212/3047034661_f96548965e_s.jpg" height="75" width="75" alt="Time Square Awe" /></a></li>
          <li><a href="http://www.flickr.com/photos/remysharp/3047034531/" title="Wonky Buildings"><img src="http://farm4.static.flickr.com/3022/3047034531_9c74359401_s.jpg" height="75" width="75" alt="Wonky Buildings" /></a></li>
          <li><a href="http://www.flickr.com/photos/remysharp/3047034451/" title="Leaves of Fall"><img src="http://farm4.static.flickr.com/3199/3047034451_121c93386f_s.jpg" height="75" width="75" alt="Leaves of Fall" /></a></li>
        </ul>        
      </div>
    </div>
</body>
</html>

Comments

  1. Matthew Burton On 6th August 2009 at 20:08

    +1 for autoscrolling. I’d like to replace jCarousel with this (it’s much simpler), but would love for it to have this one extra capability. Possible?

    Thanks for all the time you’ve put into this.

  2. Andrew On 7th August 2009 at 00:08

    Does anyone know if there is a fix to iE7?

  3. David On 8th August 2009 at 04:08

    This site is AWESOME! Keep up the good work. And thanks!

    Great posts & information.

    David

  4. Matthew Burton On 9th August 2009 at 03:08

    Here’s a real puzzler: my carousel seems to crap out after 19 slides. I’ve tried this over and over with different slides, and still: same result. Check it out:

    http://dev.globaleese.org/carouseltest.php

  5. Remy On 11th August 2009 at 11:08

    @Reece - if you subscribe to the Podcast via iTunes they’re all encoded to be iPhone compatible (though these should be too, I’ll check that out) - so you can even have them offline if you’re on the Podcast subscribe list.

    @Matthew Burton - I suspect you’re hitting the width of the ul element container. It currently has a width of 9999px - just up that value and it should prevent the wrapping.

    @Andrew and others - IE7 issue. I’m not sure what you’re seeing. I do try to test all the tutorials in IE6, IE7, Firefox, Safari and Opera. I’ve seen issues where the code changes over time and it breaks (the slider tutorial is suffering from this right now), but this demo definitely works in IE7 - I’ve even recorded a quick screencast today showing it:

    http://jqueryfordesigners.com/media/infinite-carousel-ie7.mov (QuickTime only - 500Kb)

    @Others - re: autoscrolling - I’ll post up some code for this shortly.

  6. Joe On 12th August 2009 at 15:08

    Really nice work. A little over my head (this was my first time ever seeing a jquery plugin being built) but really great tutorial.

  7. Iklan Baris On 14th August 2009 at 14:08

    Nice tutorial for design. Thanks for share..

  8. Kevin On 19th August 2009 at 13:08

    Ahhh…Frustrated!!!

    http://www.adcapnet.org > home page bottom right.

    1. The slider doesn’t expand the size of the div it’s in. I can manually adjust the size of that div to fit the carousel but I’d rather have the carousel expand the div on it’s own just like text would.

    2. I don’t see the arrows. The carousel functions if you click where the arrow is supposed to be but I see no arrows.

    Please help!!!

  9. Anoop On 22nd August 2009 at 07:08

    Nice effort anyway its not rendering in IE-6 ….. and lately i had a look at Chris Coyier’s “anything slider” .. but didnt got time to go completely through it …

  10. Pieter Van Waeyenberge On 24th August 2009 at 16:08

    Hello

    Nice article, thnx for that.

    I have recently ‘discovered’ jQuery, and was doing some research on available (infinte) scrollers. Any idea why most scrollers scroll either item by item, or itemset by itemset … instead of pixel by pixel, controlled by mousemovement? I understand it may be more difficult to program, but doable i think. Have you explored this option? Is there some constraint i am overlooking?

    Also, why not add a new node (e.g. left) per node that disappears (e.g. right) from the visible DOM (e.g. when scrolling from left to right) ? That way, when there are lots of items, you dont have to preload the whole item set. Or would that be too processor intensive?

    Cheers, Pieter.

  11. carlos On 27th August 2009 at 18:08

    for me, it does not work over ie6, but i try to delete position: absolute; on .infiniteCarousel .wrapper ul {} and now works perfect on ie6. Thanks Remy

  12. Suraj On 2nd September 2009 at 05:09

    Huge thanks for posting this invaluable tutorial.

    As a newbie to html/css please can you tell me how I can insert my own left and right arrow graphics. I’ve tried replacing the background image under the .infiniteCarousel. arrow CSS rule but only the right arrow appears (and disappears on rollover) and there is no left arrow to be seen.

    Hope you can assist!

    Many thanks Suraj

  13. John Irvine On 5th September 2009 at 17:09

    Works fine in firefox…but have a look when you try to view it in IE7…i get the arrows but no carousel?? any ideas?

  14. Fabrice Delaneau On 6th September 2009 at 11:09

    Hi,

    Great tutorial, perfectly explained except for the relative scrollLeft value : scrollLeft : ‘+=’ + left

    You go over this in a sec and I would love to understand exactly what kind of woodoo this is. It seems to me that the ‘left’ value should be enough since it has already the direction embedded a few lines above.

  15. Anush Shetty On 7th September 2009 at 08:09

    I would like to add a fading effect after a fixed interval of time. So it basically the automatic in finite carousel but the elements appear with a fade effect. Any pointers ?

  16. Billy On 8th September 2009 at 04:09

    Great tutorial, but I have a question I can’t find answered anywhere!..

    I am designing a portfolio site for a client, and she wants different categories for which I am using Carousels in the folio index to list the pieces, however the carousel seems to not rotate properly for the longer lists, so while the carousel works on it’s own, if I have multiple lists it seems to just take the amount of pages of the last loaded list, and apply it to all lists on the page (four, one for each category).

    The lists work perfectly fine on their own, but even making the ul a seperate class for each list I want displayed seems to get the list amount or pages for the last clicked on list, or last loaded class/function. Any ideas how/if this can work? It’s driving me insane and is the last thing that needs to be fixed up with this site before it’s good to go and I know next to zero about JS/Jquery to really know where to turn!

  17. Davorin On 8th September 2009 at 10:09

    Very nice carousel, but doesn’t work in IE, debugging it right now ;)

  18. Davorin On 8th September 2009 at 12:09

    Sorry, didn’t see the comments, if you replace min-height with height for IE6 it works perfect!

  19. mfs On 16th September 2009 at 21:09

    Hello - I have this almost working! but the arrows for nav do not show up, was wondering if you might give me insight - or direct me where to find the solution.

    Thanks!

  20. Mel On 18th September 2009 at 19:09

    In the .css, I have increased the size of the class “.infiniteCarousel .wrapper” from width: 315px to width: 620px. When I preview the carousel (have auto added), the images don’t cycle through all the way. Only the first three images are being cycled on both the auto and when buttons are pressed. I’m thinking the jQuery is not liking the size, because it works fine at width: 315px. Do you have any suggestions on how to fix?

  21. Selva Comments On 21st September 2009 at 10:09

    Awesome plugin!! great job. Tested it and I’m really digging it. can’t wait to use it on my live site!!

    superb work!

  22. mfs On 21st September 2009 at 20:09

    re: my previous post, I just had to change the css positioning of the direction arrows and they now show up, thanks for the script, it seems to be working beautifully (and I’m actually pulling in data from a database to display with links)

  23. Marty On 22nd September 2009 at 02:09

    Is it possible to make this a vertical scroll, instead of horizontal?

  24. j On 23rd September 2009 at 16:09

    Yeah is it possible to make this vertical scroll?

  25. Ben On 29th September 2009 at 21:09

    Works great in FF / IE8 / IE6 but not IE7, infact, it tears the script apart in IE7 Anyone got a fix for this?

Leave your own comment
  • http://