Articles

Variable Width Slideshow

Posted by & filed under Code.

Recently I was asked by a designer to create a slideshow where the images are of variable width, each image needs to center in the frame of the slideshow, and the control panels on the left and right need to animate to adjust for each image. I think this creates an attractive, sleek effect and could be quite usable by other folks, so turning it into a jQuery plugin is the next step…but for now, here’s an explanation of how I created it. First, check out the final version:
  • Next Picture
  • Previous Picture
  • Pause
  • Play
  • 1 1/2" Premium Wide Plank Cherry Countertop, Fruitwood #2 Color, Eased Square Edge
  • 2 1/4" Premium Wide Plank Teak Countertop, Elizabethan Brown #4 Color, Ogee "F" Edge
  • 1 1/2" Premium Wide Plank Zebrano Countertop, Natural #1 Color, Eased Square Edge
  • 2 1/4" Premium Wide Plank Cherry Countertop, Natural #1 Color, Ogee "F" Edge
  • 1 1/2" Premium Wide Plank Cherry Countertop, Fruitwood #2 Color, Eased Square Edge
  • 2 1/4" Premium Wide Plank Teak Countertop, Natural #1 Color, Ogee "F" Edge
  • 1 1/2" Premium Wide Plank Cherry Countertop, Fruitwood #2 Color, Eased Square Edge
Previous Image
Next Image

Current image description


Right. So, before we get into any jQuery fun, we need to do a little markup and styling. However you can view the complete script here if you like. First, let’s put down some markup for the nav:


<div id="slideshow-header"> 

    <ul>  

         <li class="control" id="mycarousel-next">Slide Right</li>  
         <li class="control" id="mycarousel-prev"> Slide Left</li>  
         <li id="index-display"></li>  
         <li id="pause-button">Pause</li>  
         <li id="play-button">Play</li>  
      
    </ul>

</div>

Looking at this markup, my first thought is, that’s quite a few id’s and classes, more than you’d like to see. However, because of the functionality of the slideshow, it helps to have a hook for each list-item in the nav. We could use jQuery to target the play and pause list items without id’s, but that means more javascript, so I think it’s a reasonable trade-off for the functionality.

Here’s the CSS for the nav:

#slideshow-header ul {
	width:auto;
	float:right;
	}
	
#slideshow-header ul li {
	float:right;
	display:block;
	list-style:none;
	margin-left:30px;
	width:auto;
	cursor:pointer;
	}
	
#slideshow-header ul li#index-display {
	cursor:default;
	width:30px;
	}
	
#slideshow-header ul li#mycarousel-next {
	background:url('http://www.jontakiff.com/brooks
       /images/products/slide-top-right.gif') no-repeat;
	text-indent:-3000em;
	width:11px;
	height:19px;
	}
	
#slideshow-header ul li#mycarousel-prev {
	background:url('http://www.jontakiff.com/brooks
        /images/products/slide-top-left.gif') no-repeat;
	text-indent:-3000em;
	width:11px;
	height:19px;
	}

Pretty simple stuff. I added a width to the index-display because if it is set to auto, when the numbers change, the difference in character widths will change the width of the list item, and the items to the left will jump a bit to the right or left as the character widths change. So setting a fixed width for it takes care that issue. However, we do have an accessibility issue to deal with. The list-item for the image counter is empty, so we need to figure out how to get some semantic content in there. I made the following changes:


<li id="index-display"><span>Slideshow Image Counter</span></li>

#slideshow-header ul li#index-display span {
	display:none;
	}

There may be a more accessible way to do this, however, I’m going to leave that to a future update. For now, there’s a meaningful term within the list item, it doesn’t break the layout and it can be accessed by a screenreader.

Next thing to do is set up some markup for the body of the slideshow. Here is the structure:


<div id="product-slideshow">
<div id="product-slideshow-inside">
<ul id="mycarousel">
<li><img1 /></li>
<li><img2 /></li>..etc
</ul>
<div id="slide-left-nav" class="control-parent control">
<div id="slide-left-nav-button">
</div>
</div>
<div id="slide-right-nav"class="control-parent control">
<div id="slide-right-nav-button">
</div>
</div>
</div></div>
..and some styling…

#product-slideshow {
	border:1px solid black;
	height:335px;
	width:624px;
	padding:2px;
	}
	
#product-slideshow-inside {
	height:335px;
	width:624px;
	position:relative;
	}
	
#mycarousel {
	overflow:auto;
	position:relative;
	}
	
#mycarousel li {
	display:block;
	}

#slide-left-nav {
	background:url("http://www.jontakiff.com/brooks
        /images/products/slide-transparent-alt.png") 
        left top repeat-x;
	position:absolute;
	height:335px;
	left:0;
	top:0;
	border-right:2px solid #fff;
	}

#slide-left-nav-button {
	background:url("http://www.jontakiff.com/brooks
        /images/products/slidearrow-left-alt.png") 
        left bottom no-repeat;
	float:left;
	margin-left:25px;
	margin-top:150px;
	}

#slide-left-nav-button, #slide-right-nav-button {
	height:30px;
	width:22px;
        text-indent:-3000em;
	}

#slide-right-nav {
	background:url('http://www.jontakiff.com/brooks
         /images/products/slide-transparent-alt.png') 
         left top repeat-x;
	position:absolute;
	height:335px;
	right:0;
	top:0;
	border-left:2px solid #fff;
	}
	
#slide-right-nav-button {
	background:url('http://www.jontakiff.com/brooks
       /images/products/slidearrow-right-alt.png') 
      left bottom no-repeat;
	float:right;
	margin-right:25px;
	margin-top:150px;
	}

Again, simple CSS. One thing to notice, in that in the declarations for #mycarousel li, I haven’t floated the list items to the left. I’d rather they display vertically if Javascript is turned off. It’s more graceful to have to scroll down rather than across…so those list items will be floated using jQuery. I’m using positioning contexts to place the left and right panels in the left top and right top corners. Also, the ul #mycarousel is position:relative as well. This is because we are going to be calculating left offset values for how much the this unordered list needs to move based on the width of the image adjacent to the one centered in the viewport .

The last piece of markup is the container that will display the alt attribute information for each image. Again, we’ll add a span to it for accessibility, using display:none; as our style declaration.

<p id="display"><span>Current image description</span></p>
Right! That about does it for the markup and styles. Now let’s take a look at the jQuery that makes the magic happen. Here’s the first block of code:

jQuery(document).ready(function($) {	
	$('#mycarousel li').css('float','left'); 
	$('#product-slideshow-inside').css('overflow','hidden'); 
	var n=$('#mycarousel li').length;
	$('#index-display').append('1/'+(n-1));
	var parentwidth=0;
	$('#mycarousel li').each(function(){
		parentwidth+=$(this).width();
		});
	$('#mycarousel').css('width',parentwidth);

There are different methods to get jQuery to work when there’s conflict with another library (such as prototype or scriptaculous.) The first line of code has worked well for me on WordPress sites, while the shorthand version ($function() {}); almost always gives me a problem. There are couple of other things to notice up front. The first line floats the li’s (containing the images) left, so that without javascript they’ll fall vertically as we mentioned before. Same concept is in place for #product-slideshow-inside. We can’t set the overflow:hidden rule in the CSS otherwise with javscript off, you would only see that first image displayed.

Next we start organizing the slideshow’s logic. We assign a variable “n” to a number which equals the number of list items that are contained within the unordered list #mycarousel. Then we subtract one from that number because of a slight quirk in the markup. When you navigate to the last image, it doesn’t look great if there is a blank panel to the right, so I’ve just added the first image again to the end of the list. This is not very semantic, and in the next version I should append that extra image using jQuery, but for now, we subtract one from the variable to indicate the unique number of images. This number is then appended to #index-display so the user sees the total number of unique images available. The next task is to set the total width of the unordered list. We want the unordered list to be the same width as total width of all list items, so, we use the each() command to add the width of each list item to the variable parentwidth. Finally, we use the value of parentwidth to set the width of #mycarousel. Let’s move on to the next block:


var firstoffset=$('#product-slideshow-inside').width()
-$('#mycarousel li:first').width();
	var theoffset=firstoffset/2;
	$('#mycarousel').css('left',theoffset);
	$('.control-parent').css('width',theoffset);
	var firstcaption=$('#mycarousel li:first').children()
        .attr('alt');
	$('#display').text(firstcaption);
	$('#display').prepend('In this picture:
');

Ok. So, we are going to need to set some rules to determine how the slideshow will display when the page is first loaded. This portion uses some very simple math meant to center the first image and slide panels properly. We take the width of the entire slideshow, subtract the width of the first list item, divide that by two and assign it to a variable. We're dividing by two because after we subtract the width of the list item, the remainder will be split evenly between the left and right panel. We then move the entire unordered list (#mycarousel) to the right by that same amount. Finally, we grab the alt attribute from image within the list item (by using the children() command) and prepend it to the #display paragraph under the slideshow.

This next block of code causes the black arrows in each panel to either display or hide, depending on the state of the slideshow:


$('#product-slideshow').hover(
function() {
if (currentPosition==1) 
{
$('#slide-left-nav-button').css
('background-position','left bottom');
$('#slide-right-nav-button').css('background-position','left top');
}
else
{
$('#slide-right-nav-button').css('background-position','left top');
$('#slide-left-nav-button').css('background-position','left top');
}
},

function() {
$('#slide-right-nav-button').css
('background-position','left bottom');
$('#slide-left-nav-button').css
('background-position','left bottom');
}
);

But Jon, you say, what's the deal with the currentPosition variable? Where is that coming from? Good question. It's a global variable that I actually declare further on in the script and its value is set to 1. So that being the case, when you hover on the slideshow as soon as it loads, the left panel's button will not show. I have used the very well known pixy technique here to hide and show button images. However if currentPosition is not equal to 1, then hovering over the slideshow will cause both arrows to be visible. Now, the next bit is where it starts to get interesting...


var Sliderules = 
{
slideactions:function(){
$('#index-display').empty(); 		
$('#index-display').append(+currentPosition + '/'+(n-1)); 
currentItem=$('#mycarousel li').eq(currentPosition-1);  
itemLocation=currentItem.position();
leftdistance=itemLocation.left; 
totalOffset=$('#product-slideshow-inside')
.width()-currentItem.width();
var panelOffset=totalOffset/2; 
var currentRounded=Math.round(panelOffset);
$('.control-parent').animate({
width:currentRounded},600) 
totaldistance=itemLocation.left-panelOffset; 
$('#mycarousel').animate({
left:-totaldistance  
},600,function(){		
$('#display').empty();
var imagecaption=$('#mycarousel li')
.eq(currentPosition-1).children().attr('alt');
$('#display').text(imagecaption);
$('#display').prepend('In this picture:
'); }); } };

So here we've created an object (Sliderules) and a method for the object (slideactions). The method is a function that represents a block of commands that are common to both sets of slideshow controls - the red buttons up top as well as the right and left panels. When you click on a navigation panel or button, or when the slideshow runs automatically, there is a function call. The first thing that happens inside that function is that the value of currentPosition gets incremented by 1. After this happens, the slideactions() method is then called. Since the currentPosition value has now been incremented, the calculations here are being made on the image to the right of the centered image, before it moves into the viewport. So, the currentItem variable represents the next image (currentPosition-1, to offset for the ordinal index). The x-y position values of the currentItem are assigned to itemLocation, and the leftdistance variable is assigned the left offset value. These offset numbers are all relative to the containing div, since it was set to position:relative. Next, we subtract the width of currentItem from the container width, and divide that by two to determine how wide each panel will need to be, round that number off, and set both panels to animate to that width. We need to figure out exactly how far #mycarousel has to move, which we can do by taking the value of leftdistance (how far the current item is from the left) and subtracting the width of one of the panels (which we just calculated for panelOffset). Then we make that figure negative, so #mycarousel will be pulled to the left the proper distance. Finally, we add the image caption. In this next block we'll see how this function is called.


var currentPosition = 1; 
function slidestuff() {
var n=$('#mycarousel li').length;  
currentPosition+=1;  
if (currentPosition >=n ) 
{
currentPosition=1;
}	
Sliderules.slideactions();	
}
var runfirst;
function runit() {
runfirst=setInterval(slidestuff,6000);
}
runit();
	
$('#play-button').click(function() {
clearInterval(runfirst);
runit();
}); 
		
$('#pause-button').click(function() {
clearInterval(runfirst);
});

$('.control').click(function() {
clearInterval(runfirst);
 });

So here is where I have declared var currentPosition=1. So currentPosition is a global variable, meaning that when I use it locally inside a function, because it isn't defined, it will go up the scope chain and change the global variable's value. I'm aware that this isn't necessarily the best way to operate, but here I think it makes sense. Ultimately I want to be able to change the global value of currentPosition from three different functions, so that the right position will be maintained no matter which slide controls you use. So here you can see the global currentPosition variable being incremented. Also we have an if statement which says if the value of currentPosition is higher than the number of items, go back to item number 1. Then we call the slideactions method of the Sliderules object. So this makes the slideshow start automatically and change images every 6 seconds if you don't do anything. Below that, the interval is cleared if any of the controls are clicked. Finally, here's the last piece...


$('.control').bind('click',function() {
var n=$('#mycarousel li').length;
$('#pause-button,#play-button').css('color','#403d3c');
currentPosition=($(this).attr('id')=='slide-right-nav')||
($(this).attr('id')=='mycarousel-next') ? 
currentPosition+1 : currentPosition-1;
if (currentPosition >= n)
{
currentPosition=1;
}
		
if (currentPosition < 1)
{
currentPosition=1;
}
if (currentPosition==1)  
{
$('#slide-left-nav-button').css('background-position','left bottom');
}
if (currentPosition==2)
{
$('#slide-left-nav-button').css('background-position','left top');
}
Sliderules.slideactions();	
 });
	
});               

Ok! Not too bad! So here, we are writing the logic for the slide panels and buttons. First, we get the length and assign it to var n. Next, make sure the play and pause button colors are dark grey. Then an or statement which either increments or decrements 1 from the value of currentPosition (so you can go back in the slideshow). Finally, we add a little touch in at the end that will display the left arrow once you are past the first image...this is a small embellishment and acts a visual cue. Finally, we call our good old slideactions() method, and then go have a cup of tea. I'm sure this script has room for improvement! Let me know what you think...

12 Responses to “Variable Width Slideshow”

  1. jb

    Hey man, awesome work there, you didn’t provide a download package though. Are we allowed to grab it and use in production?

    I’ve also got a specific request, what would it take to have this 100% and have a “infinite” slide?

    Reply
  2. admin

    Hey, thanks for the comment. You can go ahead and use this in production…it would be great if you left a link, but you don’t have to. As far as your other requests…if you wanted a 100% width on the slideshow, I’d try just setting the width on the parent and inner containers to 100%, which should just set it to the width of whatever the parent element is. I don’t see why it wouldn’t work. As far as the infinite slide, that’s a useful idea, I’ll post back here if I come up with something. If you do use it, send me a link, I’d love to see it in action.

    Reply
  3. jb

    It worked perfectly with the 100% width (I firebugged your site already hehe) but when the first image is in focus there’s a huge unfilled space to the left hence the need for the infinite thingy

    Reply
  4. admin

    You’re right, it would be probably be sharper to be able to go back and forth. There’s a gap to the left which I can see wouldn’t look great if the slideshow was very wide. One option might be to put a background color, or background image (like x’s or repeating lines) on the parent slideshow element. That way it would be clear that there was no image to the left and it would be covered as soon as the slideshow moved. It’s a short term solution but like I said, I may play around with that….possibly later in the week.

    Reply
  5. jb

    Ah yes Good tip on the background. I messaged you on facebook. Will definitely keep intouch. And thanks for this btw saved me big time. Sweet site too!

    Reply
  6. admin

    Awesome! Love to see how you are using it…

    Reply
  7. matthew

    Love this — just saved me a whole lot of headache. I’m also using at 100% browser width. Thanks so much!

    Reply
  8. admin

    Awesome Matthew, glad you are finding it useful. I will check out your site when I get a chance, looks pretty interesting. -Jon

    Reply
  9. Matthew

    Question: is it possible for this to carousel wrap around continuously rather than animate back to the first slide?

    Reply
  10. Francis

    I’m also looking for this affect with a continuous loop. Any chance of additional support / tutorials?

    Reply
  11. admin

    I have been busy folks, but I agree this would be great with the wrap…I will keep you posted if I get time to get that going.

    Reply
  12. Martijn

    Can you at least indent your code? It’s nearly unreadable like this…

    Reply

Leave a Reply

  • (will not be published)

XHTML: You can use these tags: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>