Building a Lightbox with jQuery

6 May 2009

This article has become very outdated and will not be getting updated anytime soon.

I often get the question, “what lightbox do you use?”, to which I respond, “the one I built last week.” Then earlier today I mentioned to someone on Twitter that building a lightbox is a good way to learn jQuery. That got me thinking that I should write a tutorial, and here it is.

What Is a Lightbox?

Rather than assume that you know what I mean by a lightbox, I’ll briefly explain it. The first experience I had with this UI pattern, and I believe the first real example of it, was by Lokesh Dhakar in 2005. Since then, this pattern of simple, unobtrusive script used to overlay images on the current page huddletogether.com is everywhere. It’s hard to find a modern site without some variant of the in-page popup element over a semi-transparent background.

Demo

Dublin CoastBulmers. Cheers Mate!Central
DublinGuiness for Strength

The Markup

<ahref="http://farm1.static.flickr.com/54/146541041_ccdf28d640_o.jpg"class="thumb"rel="lightbox"title="Dublin Coast"><imgsrc="http://farm1.static.flickr.com/54/146541041_ccdf28d640_s.jpg"alt="Dublin Coast"/></a><ahref="http://farm1.static.flickr.com/50/146540504_c4ef35d370_o.jpg"class="thumb"rel="lightbox"title="Bulmers. Cheers Mate!"><imgsrc="http://farm1.static.flickr.com/50/146540504_c4ef35d370_s.jpg"alt="Bulmers. Cheers Mate!"/></a><ahref="http://farm1.static.flickr.com/55/146540016_2532ca7263_o.jpg"class="thumb"rel="lightbox"title="Central Dublin"><imgsrc="http://farm1.static.flickr.com/55/146540016_2532ca7263_s.jpg"alt="Central Dublin"/></a><ahref="http://farm1.static.flickr.com/52/146539726_2530fe446b_o.jpg"class="thumb"rel="lightbox"title="Guiness for Strength"><imgsrc="http://farm1.static.flickr.com/52/146539726_2530fe446b_s.jpg"alt="Guiness for Strength"/></a>

In the spirit of progressive enhancement we want to make sure that these links still work without a user having javascript enabled. Each link points to the fullsize file, has a title attribute that we will use as a caption in a future tutorial, and a rel attribute set to “lightbox”. More advanced versions of this script segement different images into groups by extending the rel attribute even more, but that’s for another tutorial.

The Script

Prerequisites

Since this is a jQuery based script, we need to load in the jQuery library first. To do that we add <script src="/js/jquery-1.3.2.min.js"></script>. For performance reasons, I put that tag just before the </body> tag. Then we need to put our custom script in a file and load it in by placing <script src="/js/lightbox.js"></script> just after our jQuery script tag.

Calling the function

The first thing we want to do in our lightbox.js file is set our lightbox function to fire as soon as the DOM is ready. Fortunately, that is very simple in jQuery.

$(document).ready(function(){lightbox();});

Creating the lightbox function

Now we need to setup our lightbox function itself and add some markup to the page.

functionlightbox(){varlinks=$('a[rel^=lightbox]');varoverlay=$(jQuery('<div id="overlay" style="display: none"></div>'));varcontainer=$(jQuery('<div id="lightbox" style="display: none"></div>'));vartarget=$(jQuery('<div class="target"></div>'));varclose=$(jQuery('<a href="#close" class="close">&times; Close</a>'));varprev=$(jQuery('<a href="#prev" class="prev">&laquo; Previous</a>'));varnext=$(jQuery('<a href="#next" class="next">Next &raquo;</a>'));}

The first thing to look at here is how we actually find all of our links. jQuery supports the full gamut of CSS3 selectors. So we use $( 'a[rel^=lightbox]' ). This says get any link with a rel attribute that starts with “lightbox”. The ^ part tells jQuery to match anything that starts with the matching string instead of having to match it exactly. We could also use $( 'a[rel=lightbox]' ) but using the ^= selector will come in handy when we want to group the images in the aforementioned more advanced tutorial.

The overlay and container variables are what makes the translucent background and white container box respectively. At this point they are not yet inserted into the page, we’re just setting them up. jQuery really makes this part simple since we can just pass in arbitrary markup and it takes care of all the DOM injections. Traditional javascript looks like this:

varoverlay=document.createElement('div');overlay.id='overlay';overlay.style.display='none';

As you can see, the jQuery version is much cleaner and leaner.

The target is basically a placeholder div where will we put our images as they are loaded. We could just put them directly in container but in my experience this extra div saves a lot of headache further on.

close, prev and next are links that serve as our navigation inside the lightbox. The demo here just leaves them as text but it is easy enough to use CSS to do image replacement on them if you want a more graphical look.

Now that we have all of our markup ready we can start injecting it into the page.

$('body').append(overlay).append(container);container.append(close).append(target).append(prev).append(next);container.show().css({'top':Math.round(($(window).height()-container.outerHeight())/2)+'px','left':Math.round(($(window).width()-container.outerWidth())/2)+'px','margin-top':0,'margin-left':0}).hide();

The first part here is adding the overlay and container into the page. Because of jQuery’s chaining ability we can do multiple append() calls in a row and each item is appended to original object ( $( 'body' ) in this case ). The second line appends all of the control elements inside of the container.

The third line might seem a bit tricky. The idea here is to figure out a) how big is the container just from the CSS, and b) what the left and top positions need to be to center the container in the viewport. But since the container is hidden by default, its width and height will return as 0. So the first thing we do is call show() on it so the width and height return meaningful values. Next we want to figure out the top and left. It’s pretty simple math but looks a little complex written out. All we are doing here is subtracting the width of the container from the width of the window then dividing the result by 2 so the we have even spacing on either side. We call Math.round() on all of that just for good measure then append px to it just to be safe. Then we repeat all of that process for the heights as well. Now that we have the position all being handled by top and left, which we need to do in order to animate the position (we’ll cover that later on), we zero out all of the margins. Finally, we hide the object again. Because of the speed of jQuery, all of this takes place without the container ever being seen by the viewer.

Event handlers for the control objects

close.click(function(c){c.preventDefault();overlay.add(container).fadeOut('normal');});prev.add(next).click(function(c){c.preventDefault();varcurrent=parseInt(links.filter('.selected').attr('lb-position'),10);varto=$(this).is('.prev')?links.eq(current-1):links.eq(current+1);if(!to.size()){to=$(this).is('.prev')?links.eq(links.size()-1):links.eq(0);}if(to.size()){to.click();}});

The close part is straightforward. By using click( function( c ){... we can access the click event itself through the variable c. Thus, the first thing we do in our function is call c.preventDefault() which stops the link’s default action ( looking for the #close anchor tag ) from executing. The add() function in jQuery lets us group together different objects and let us act on them all at once. Thus we are fading both the overlay and the container at the same time in one call.

Now we group prev and next together and handle their click events in one function. We could do them separately, but I try to follow the DRY principle. It keeps your code leaner and makes later changes to behavior easier to implement. Again we use c.preventDefault() to stop the link’s hrefs from being followed. Then we need to determine which image is currently being shown. There are a few ways to do this, but I like to add a “selected” class to the links when they are clicked. This way if we want to style the selected thumbnail differently, we already have a class in place. We set the variable current to the value of the current thumbnail’s lb-position attribute. ( If you’re wondering where that comes from, we’re almost there. ) We want to run that value through parseInt() in order to insure that the result is a number instead of a string or boolean since we’re about to do some math with the value.

The next step is to determine which image should be displayed now. We set this image to the variable to and use a shortcut if…else statement to determine if we need to look forward or backward. If you’re not familiar with this shorthand style, it works in the format ( condition ) ? doiftrue : doiffalse. ( See the last section of this page for more info. ) Our statement checks if $( this ), which in our case is the link that has been clicked, is the previous link. If it is, we subtract 1 from current to determine the proper index in the links object that we want. If it is not, we add 1. We then want to check to see that the link we’ve asked for actually exists. If it doesn’t it means we’re either at the beginning or end of the list, so we do another conditional statement to either get the last link ( if we are at the first and want to go to the previous one ), or get the first link ( if we are at the last and want the next one ). Then just for belt and suspenders, we check once more to make sure to actually exists before we call click() on it. This prevents our click() call throwing an error if to somehow isn’t an object. Notice how we really don’t do anything substantial here, we’re just going to use the same click() event that we will define in the next step to handle changing the actual images.

links.each(function(index){varlink=$(this);link.click(function(c){c.preventDefault();open(link.attr('href'));links.filter('.selected').removeClass('selected');link.addClass('selected');});link.attr({'lb-position':index});});

Now it’s time to handle the links themselves. We do an each() loop on them. Inside our loop function we define a click handler for each one. It does our normal c.preventDefault() method, then calls our open() function passing in the href of this link. Then we handle removing the selected class from the other links and adding it to this one. Finally, outside of the click handler, we set the lb-positon attribute that we referenced before.

Opening the container and overlay

varopen=function(url){if(container.is(':visible')){target.children().fadeOut('normal',function(){target.children().remove();loadImage(url);});}else{target.children().remove();overlay.add(container).fadeIn('normal',function(){loadImage(url);});}}

The open() function has one purpose: determine if the container is already showing and if it’s not, show it before we start loading the image. If it is showing, we fade out anything already in target and remove those elements before moving on.

Loading the image

varloadImage=function(url){if(container.is('.loading')){return;}container.addClass('loading');varimg=newImage();img.onload=function(){img.style.display='none';varmaxWidth=($(window).width()-parseInt(container.css('padding-left'),10)-parseInt(container.css('padding-right'),10))-100;varmaxHeight=($(window).height()-parseInt(container.css('padding-top'),10)-parseInt(container.css('padding-bottom'),10))-100;if(img.width>maxWidth||img.height>maxHeight){// One of these is larger than the windowvarratio=img.width/img.height;if(img.height>=maxHeight){img.height=maxHeight;img.width=maxHeight*ratio;}else{img.width=maxWidth;img.height=maxWidth*ratio;}}container.animate({'width':img.width,'height':img.height,'top':Math.round(($(window).height()-img.height-parseInt(container.css('padding-top'),10)-parseInt(container.css('padding-bottom'),10))/2)+'px','left':Math.round(($(window).width()-img.width-parseInt(container.css('padding-left'),10)-parseInt(container.css('padding-right'),10))/2)+'px'},'normal',function(){target.append(img);$(img).fadeIn('normal',function(){container.removeClass('loading');});})}img.src=url;}

Here’s our final part: the loadImage() function. Take a breath, it’s going to be fine, we’ll get through this together. The function takes one argument of url to tell us which image to load. The first line checks to see if we are already loading an image; if we are we return to keep from going further. Then we add the loading class, which with some CSS and a spinner from ajaxload.info gives some nice user feedback that something is happening.

The next part is traditional javascript and not jQuery. After a lot of tinkering I discovered that the only way to get an image’s onload event to fire across all the major browsers is to do it this way. First we set up the variable img as a clone of the Image object. Then we set it’s onload handler by setting img.onload = &hellip;. We want this image to be hidden so we can fade it in when we’re done, thus img.style.display = 'none'. The next several lines deal with two things: making sure the image fits inside the viewport and resizing the container to the size of the new image. The first thing we need to do is determine our maxWidth and maxHeight which are similar to the formulas we used earlier to position the container. The idea here is we take the viewport width, subtract the width of the horizontal padding of the container and then subtract 100 more to give us some outside margin. 100 is an arbitrary value that you can set to whatever suits you. Now that we have our maximum dimensions we check to see if the image is larger than either one. If it is, we set the image dimensions to fit inside the viewport, using the aspect ratio to determine the shorter side.

With the image properly sized, we can resize the container to this new target size with jQuery’s animate() function. If you’ve never used this function before, you pass in an object with all the target CSS values ( except for a few like margin that don’t work ) and jQuery will determine how to get the object there over time. We tell it we want our animation speed to be normal but we could also use fast, slow, or give it how many milliseconds we want the animation to take. The final argument for animate() is a callback function that gets run when the animation is complete. In our case, at that point we append the img to the target then fade the img in at normal speed and remove the class “loading” from target once that’s done.

Finally, after our onload function, we set the src attribute for img. It’s important that we set this after onload or Internet Explorer will never trigger our onload event.

Conclusion

I hope you we’re able to follow along and learn something from this tutorial. You can download both the script and the CSS that I used on this page to review. Unfortunately I am unable to offer support for this script as it’s intended as an educational example and not an end product. However, my services are available for hire; please contact me if you’re interested.