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. Chris Greenhough On 16th June 2009 at 08:06

    @danielre, I’ll be honest, I haven’t looked at this code in any detail, but I think the images are just list elements (well, links within them) inside an unordered list, so I can’t see any reason why it wouldn’t be possible to edit in wysiwyg mode in dreamweaver or similar… If you wanted the contents to be dynamic (e.g. you upload a folder of pics and the carousel is built automatically) that would require some php I would suggest.

  2. Ganeshji Marwaha On 16th June 2009 at 11:06

    Great tutorial and crisp explanation. Just for information purposes, you might want to know that

    jCarouselLite (written by me) is a very light-weight jquery plugin. It has infinite carousel (circular) as one of its options. It supports a lot of other options too, just in 2 KB. Please take a look at it…

  3. Greg On 19th June 2009 at 20:06

    Really enjoyed watching you pulling those Jquery functions out of the hat without even blinking… Until now I was just messing around with the Jquery functions, but now I’ll get it serious.. What is your opinion about calculating the ‘left’ variable in gotopage() like this:
    n = page - currentPage,
    left = singleWidth * visible * n;

    Is there any reason this might not work?

  4. Chandra On 21st June 2009 at 13:06

    Great tutorial / plugin. Thanks! I am testing this and somehow while testing I m getting this weird error. If I load the jquery from your server it works okay. But if I load the jquery from local (1.3.2) then the last 2 is appended before the first item thus scrolling doesn’t work. It works one time and it doesn’t on the next when I refresh. This happens in Safari. It works in Firefox. Have anyone experienced this? Is it a cache issue or something?

  5. Andrew Minton On 21st June 2009 at 21:06

    Great tutorial - Have been using jcarousel and was keen to find of if the following was possbile?

    Have a li list called from database and a carousel below using your code..

    Want to click on the link in the carousel and have the item retrieved from the database and printed to the page..

    But how to I get the carousel to hold it’s position when the new item is retrieved from the database.. mypage.php?itemid=3 will be the link and the page gets refreshed to pull in the new data.. but obviously the carousel restarts at 0 again.. the jqueryfordesiners.com site has a really nice carousel that places the currentitem as the first item in the carousel and maintains this position when you click on a new url? how to you do this? or where can I find a link to discover how I can implement it?

    cheers

    Minton

  6. Andrew Minton On 22nd June 2009 at 14:06

    HOw do you get the current active url and button to be at position 1 in the carousel?

    I have placed the code on my page, but cannot get the code fix the position of the thimbnail that I click on whatever I try?

    What is different about the code on the site to the example you give in the tutorial that is allowing you to fix the position of the first item?

    Minton

  7. Dmitry On 23rd June 2009 at 01:06

    Great post Remy! Thank you for sharing the knowledge.

    A bit off topic, I really like the color scheme in your text editor. Do you mind sharing it?

    Thanks.

  8. kim guanzon On 24th June 2009 at 23:06

    how would you apply the infinite carousel technique to the coda slider script that you made?

  9. Vizou On 25th June 2009 at 02:06

    Nice work; I’m giving it a try right now!

    And as a nitpicker myself, I would like to point out that commenter who criticized your English skills did so in very poor English him/herself – corrections below!

    “You may dismiss [THIS] as nitpicking, but attention to detail is important in design and programming, [AND] it should be in your writing[,] as well. I am very skeptical [OF APPLYING THE ADVICE] of someone who makes junior-high mistakes.”

    So there.

  10. Anonymous On 25th June 2009 at 15:06

    Does this or is this setup to support AJAX calls? So when you click right or left, you make an AJAX call to lazy load the images somehow?

  11. Peter On 25th June 2009 at 20:06

    Is there a way to make this autoscroll?

  12. Reece On 28th June 2009 at 17:06

    Rem, is there any way you can encode the QuickTime screencasts so that They can be viewed on an iPhone? For some reason they won’t play. I’d love to watch them on the go :)

  13. Lisa On 29th June 2009 at 15:06

    Very interesting article. It looks like something I might want to use later on. The problem I have with it is the gap between the images at the end. Is there a way to fix that? That’s a little awkward. Also, is it possible to make them clickable?

    One thing you might want to take a look at is another new jquery plugin by the same name strangely enough: http://www.catchmyfame.com/2009/06/04/jquery-infinite-carousel-plugin/

  14. Web010 On 29th June 2009 at 19:06

    For Andrew Minton.

    Use Ajax to load the content, that way the carousel will remain as it is.

    Or add a $_GET variable that will tell the carousel on which page the user was when he clicked the link.

    And then call the gotoPage function with that page number.

    That’s the simplest solution.

    Hope it helps.

  15. Ian Soper On 3rd July 2009 at 02:07

    Would there be a good way to have this automatically go to the next of images after a specified interval (like 7 seconds)? Btw, this was a very helpful tutorial!

  16. Adam On 3rd July 2009 at 06:07

    Thanks for this. Great tutorial and well-written code. Worked perfectly for me first time and much better than the other JQuery carousel effects I’ve seen. The only additional thing I had to do to get it working across all browsers include IE6 was to set the .wrapper height to a specific value rather than use min-height.

  17. ocean90 On 19th July 2009 at 18:07

    @Chandra I had the same problem in Opera, Firefox 3.0.10 and Safari . I fix the problem by replacing $(document).ready(function () { }); with window.onload = function () { }. Now it works.

    Thanks for this tutorial.

  18. Christian James R. Ichon On 21st July 2009 at 18:07

    This jquerry gallery really rocks :) Thanks for this nice tutorial. I already made one myself and I also customized it according to my design :) Thanks a lot for this.

  19. dlg On 23rd July 2009 at 17:07

    great site, great tuts

    any idea round the iframe problem? i was trying this with iweb and of course an html snippett is in an iframe

    tia

    damian

  20. David On 23rd July 2009 at 18:07

    Great work, however how do I set it so that you can start the carousel on a different page??

  21. Mick Dinulos On 24th July 2009 at 16:07

    Great tute. I was just wondering, how do we scroll one image at a time.

  22. Isaac On 29th July 2009 at 18:07

    This is a really excellent carousel. Was wondering if you had thought to make user preferences set up in a different way, such as:

    thumbnailstoshow = # (how many thumbnails to show in the carousel; this way less measurements needed in the style sheets. and carousel would automatically size if thumbnails change size or amount of thumbnails changes thumbnailmovement = # (how many thumbnails to slide over per button click) thumbnailpadding = #px; buttonpadding = #px; theme = “themename”; (if user has several chromes set up for different slide shows)

    have you thought about these preferences and features: thumbnail text show/hide; thumbnail text color; background color; slider speed; use of easing; use of timers/delay.

    have you found a way to overcome the blank space on non-zero MOD?

  23. Przemek On 31st July 2009 at 17:07

    Hi. Great script, I loved it until I invited smoothbox to the party. I was confused why it shows more images then there actually is, well, it’s good to read tutorial first and then use scripts.

  24. Marco On 1st August 2009 at 18:08

    Great Tutorial!

    But same problem here … don’t work with IE7 …

  25. Hina On 2nd August 2009 at 08:08

    // outer Width: width + padding (doesn’t include margin) single Width = $single.outer Width(),

Leave your own comment
  • http://