For a recent project, I needed an image gallery in JQuery. No sweat, there are hundreds of suitable plugins.
My favourite is Cycle 2. It’s flexible, HTML5-friendly, and allows you to write the markup pretty much any way you like, so you don’t have to compromise on your fancy HTML & CSS.

The designer had included two features on this slideshow, which I needed to add to Cycle 2 somehow:

  • A carousel pager – that is, a bunch of little thumbnails that scroll across, which you can click to view in the main gallery.
  • A scrollbar, which you can drag and click to adjust the current position of the slideshow.

As an additional complication, the client originally provided us with an unreasonable amount of images. The total size of the page was several megabytes, and give the speed of broadband in rural Britain in 2013 it’s just not okay to force people to download that much data the first time they load the page. So, I also had to ensure that the larger images were only loaded when requested by the user.

First then here’s the page in question, finished and hopefully working in your browser. (Not working? Please get in touch and tell me what browser you’re using!)

Required libraries:

  1. JQuery itself, the latest version at the time of writing does the job nicely.
  2. Cycle 2, including the carousel extension (swipe also recommended for mobile compatibility)
    JQueryUI
  3. And the source, starting with the HTML, which is really simple:
<h2 id="cycle-caption"></h2>
 
<div id="slideshow-1" class="row">
 <div id="cycle-1" class="cycle-slideshow auto"
 data-cycle-slides="> div.slide-container"
 data-cycle-timeout="0"
 data-cycle-caption="#cycle-caption"
 data-cycle-swipe="true"
 data-cycle-caption-template="{{alt}}">
 <div class="slide-container first" data-loaded="true" data-src="/image1.jpg" data-alt="Caption 1">
 <img src="/image1.jpg" alt="Caption 1">
 </div>
 <div class="slide-container" data-loaded="false" data-src="image2.jpg" data-alt="Caption 2"></div>
 <div class="slide-container" data-loaded="false" data-src="image3.jpg" data-alt="Caption 3"></div>
 <div class="slide-container" data-loaded="false" data-src="image4.jpg" data-alt="Caption 4"></div>
 <a href="#" class="cycle-prev"><span>&laquo; prev</span></a>
 <a href="#" class="cycle-next"><span>next &raquo;</span></a>
 
<div class="ajax-loader"></div>
 </div>
</div>
<div id="slideshow-2" class="row">
 <div class="col-xs-1"><a href="#" class="cycle-prev"><span>&laquo; prev</span></a>
 
</div>
 <div class="col-xs-10">
 <div id="cycle-2" class="cycle-slideshow"
 data-cycle-slides="> div"
 data-cycle-fx="carousel"
 data-cycle-timeout="0"
 data-cycle-carousel-visible="4"
 data-cycle-carousel-fluid="true"
 data-cycle-prev="#slideshow-2 .cycle-prev"
 data-cycle-swipe="true"
 data-cycle-next="#slideshow-2 .cycle-next">
 <div class="cycle-slide"><a href="image1.jpg"><img src="thumbnail1.jpg" alt="Caption 1"></a>
</div>
 <div class="cycle-slide"><a href="image2.jpg"><img src="thumbnail2.jpg" alt="Caption 2"></a>
</div>
 <div class="cycle-slide"><a href="image3.jpg"><img src="thumbnail3.jpg" alt="Caption 3"></a>
</div>
 <div class="cycle-slide"><a href="image4.jpg"><img src="thumbnail4.jpg" alt="Caption 4"></a>
</div>
 </div>
 <div class="slide-scroller">
 <div class="slide-scroller-tracker"></div>
 </div>
 </div>
</div>

A few things I like about this markup – it’s simple, it uses those lovely HTML5 data- attributes, and it’s easy to generate from PHP or a templating engine, assuming you are loading a list of images from somewhere. It could be grabbing all the images from a particular folder, as in my example, or it could be grabbing them from a database, or some JSON feed, it really doesn’t matter – the point is the markup is simple.
Next up, some relevant CSS. I haven’t included all the CSS used here, see the source of the demo if you need additional CSS.

.slide-scroller, .slide-scroller .slide-scroller-tracker {
position: relative;
}
.slide-scroller .slide-scroller-tracker {
width: 7%;
height: 100%;
background: #309a52;
}

This is the basis of how the positioning for the scrollbar will work. The rest of the CSS will entirely depend on how you want to lay out your gallery.
Finally, the JavaScript. This should all be inside a $(document).ready(function() { }); wrapper, but I’ve left that out to break it down bit by bit.

var slideshows = $('.cycle-slideshow');
var nextSlide = 0;

Because Cycle lacks support for progressive loading and carousel pagers together, we need to keep track of the current slide index ourselves. For my convenience, I also stuck the selector for the slideshows in a variable, so it can be adjusted without changing the rest of the code.

$('#cycle-2').on('cycle-next cycle-prev', function(e, opts) {
 // advance the other slideshow
 var currSlide = opts.currSlide;
 while (currSlide > opts.slideCount - 1)
 currSlide -= opts.slideCount;
 preloadSlide(currSlide);
});

The Cycle 2 plugin includes events called cycle-next and cycle-prev. Here, we detect when #cycle-2 (the thumbnails slideshow) is changed to the next or the previous slide, and load the appropriate slide from #cycle-1, via a method I’ll define shortly called preloadSlide();

$('#cycle-1').on('swipeleft cycle-next',function(e) {
 e.stopPropagation();
 var opts = $('#cycle-2').data('cycle.API').getSlideOpts(0);
 var index = opts.currSlide + 1;
 while (index > opts.slideCount - 1)
 index -= opts.slideCount;
 preloadSlide(index);
 });
 $('#cycle-1').on('swiperight cycle-prev',function(e) {
 e.stopPropagation();
 var opts = $('#cycle-2').data('cycle.API').getSlideOpts(0);
 var index = opts.currSlide - 1;
 while (index < 0)
 index += opts.slideCount;
 
 preloadSlide(index);
 });

Here we deal with the previous and next events on the large slideshow. These should admittedly probably be refactored to a single method, I believe I separated them during development to debug an issue which turned out to be unrelated.

Now here is our progressive loading bit. Before we let slideshow 1 progress, we check that the image has been loaded. I’ve broken this down bit by bit, with comments, so read the source carefully…

function preloadSlide(index) {
 // first, we find the slide element we're looking for - it's the nth div within #cycle-1
 // with the class slide-container, where n = index + 1
 var $slide = $('#cycle-1 > div.slide-container:eq('+(index+1)+')');
 
 // next, we check two things: 1. whether the loaded property is not true (ie. slide is not loaded)
 // and 2. due to an odd bug I was experiencing with pre-set element data, we also check whether this is
 // the 'first' slide which we have assigned the class 'first' in the HTML (see above).
 if ($slide.data('loaded') !== 'true' && !$slide.hasClass('first'))
 {
 // for a smoother user experience, we stick a "loading" swirly thing on while we load the image
 showLoadingOverlay();
 
 // create the image in memory, and when it has loaded...
 var $img = $('<img/>').attr('src',$slide.data('src')).attr('alt',$slide.data('alt')).load(function() {
 // ... when it has loaded, check the next slide we want is still the one that called this loader
 // In case of impatient users clicking lots of buttons, this may not be the case, and we should
 // only show their most recent request
 if (nextSlide === index)
 {
 // hide the loading overlay, and finally actually go to the slide we loaded
 hideLoadingOverlay();
 slideshows.cycle('goto', index);
 }
 });
 // asynchronously, this bit of code is being run, to set 'loaded' to 'true' on the slide (so we don't try to
 // load it twice), then to append the image which is currently loading in memory to the slide, and finally
 // to update the nextSlide variable to the requested index.
 $slide.data('loaded','true')
 .append($img);
 nextSlide = index;
 }
 else
 {
 // if we get to this bit of the code, the slide is already loaded, so we just go to that slide in the slideshow
 nextSlide = index;
 hideLoadingOverlay();
 slideshows.cycle('goto', index);
 }
};

showLoadingOverlay() and hideLoadingOverlay() are just what they sound like and we’ll get to those shortly.

$('#cycle-2 .cycle-slide').click(function(){
 var index = $('#cycle-2').data('cycle.API').getSlideIndex(this);
 var opts = $('#cycle-2').data('cycle.API').getSlideOpts(index);
 while (index > opts.slideCount - 1)
 index -= opts.slideCount;
 preloadSlide(index);
});

This piece of code simply means that when a user clicks on a thumbnail in the second slideshow, it loads the matching slide in the first slideshow. I found that with a wrapping carousel, the cycle API would return indices greater than the number of slides, so I added a loop to correct this behaviour.

$('#cycle-2').on('cycle-before',function(e, opts) {
 var nextSlide = opts.nextSlide + 1;
 while (nextSlide > opts.slideCount)
 nextSlide -= opts.slideCount;
 moveScrollbar(nextSlide / (opts.slideCount - 1));
 });

This piece of code handles the scrollbar. Because there are a few ways to trigger a change to the current slide in slideshow 2 and I wanted the scrollbar to be updated in all cases, I bound this change to the cycle-before event, which is triggered before the slide changes.

Except there’s a small piece of missing information from that, the “moveScrollbar” method:

var usableWidth = $('.slide-scroller').width() - $('.slide-scroller-tracker').width();
var suppressMovement = false;
function moveScrollbar(percentage)
{
 if (!suppressMovement)
 // 100% = (width of scrollbar) - (width of scrollbar tracker)
 {
 if (percentage > 1)
 percentage = 1;
 else if (percentage < 0)
 percentage = 0;
 $('.slide-scroller-tracker').css('left',parseInt(usableWidth * percentage));
 }
 suppressMovement = false;
}

My slightly misnamed variable “percentage” here refers to a number which should be between 0 and 1 (though we do handle out of bounds numbers, as you can see). The usableWidth need only be calculated once (unless the width of the scrollbar will change), so is calculated outside the method. We also add a global suppressMovement variable which I use to prevent this whole method from running if the method was triggered by the user dragging the scrollbar to a new position.

function showLoadingOverlay() {
 if ($('#cycle-1 .ajax-loader').data('visible') !== 'true')
 {
 $('#cycle-1 .ajax-loader').data('visible','true').animate({
 opacity: 1
 });
 }
}
function hideLoadingOverlay() {
 if ($('#cycle-1 .ajax-loader').data('visible') === 'true')
 {
 $('#cycle-1 .ajax-loader').data('visible','false').animate({
 opacity: 0
 });
 }

Here are those simple methods to show and hide the loading overlay. Not exactly rocket science, so let’s not dwell on those… Next though, we have the bit that makes the scrollbar draggable – this depends on JQueryUI.

$('.slide-scroller-tracker').click(function(e) {
 e.stopPropagation();
}).draggable({
 containment: 'parent',
 axis: 'x',
 stop: function(event, ui)
 {
 // when the user lets go of the scrollbar, figure out where we are in the slideshow percentage-wise
 var posLeft = $(this).offset().left - $('.slide-scroller').offset().left;
 // get percentage
 var percentage = posLeft / usableWidth;
 // we know where to scroll the slideshow to and the scrollbar is in its correct position, so prevent
 // movement when cycle-before is triggered
 suppressMovement = true;
 scrollTo(percentage);
 event.stopPropagation();
 }
 });

Now we’re getting there, the user can drag the scrollbar around, and via some unknown method called scrollTo(), it will move us to the correct slide in the slideshow.

$('.slide-scroller').click(function(e) {
 var posLeft = e.pageX - $(this).offset().left;
 console.log(posLeft);
 // get percentage
 var percentage = posLeft / usableWidth;
 
 scrollTo(percentage);
});

From the code above, like a normal scrollbar, clicking at any point on the scrollbar will move the scrollbar to that point. We simply get the position from the click relative to the scrollbar, and pass the details to that same scrollTo() method. So here’s the final piece in this puzzle: the scrollTo() method!

function scrollTo(percentage)
{
 // get num slides
 var opts = $('#cycle-2').data('cycle.API').getSlideOpts(0);
 var numSlides = opts.slideCount;
 
 var slideToUse = parseInt(percentage * (numSlides - 1));
 if (slideToUse > numSlides - 1)
 slideToUse = numSlides - 1;
 preloadSlide(slideToUse)
}

This part is really simple. We figure out the total number of slides, multiply with that “percentage” (a number supplied to us between 0 and 1) which was passed to the method, and from that we find the slide to use. Again, we have an out of bounds check, and then finally, we preload the slide if necessary, using the preloadSlide() method above!

There’s a lot of pieces to this puzzle, but hopefully by going through this code and experimenting it’ll become obvious how it all fits together. Questions? Comments? Please leave them below…

Note

In 2016 I didn’t renew a hosting contract it turned out I was using, and I had to recreate my whole blog on my new hosting. There was a really interesting, if slightly one-sided (sorry James, I’m really bad at checking comments…) conversation with a guy called James here, about handling touch events. If you’re interested in that discussion, it’s here: https://web.archive.org/web/20160322123955/https://heatherevens.me.uk/2013/10/18/tweaking-cycle-2-to-add-a-scrollbar-and-more/