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
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">× Close</a>'));varprev=$(jQuery('<a href="#prev" class="prev">« Previous</a>'));varnext=$(jQuery('<a href="#next" class="next">Next »</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.
Handling the links
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 = …. 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.



