Building a Lightbox with jQuery

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

<a href="http://farm1.static.flickr.com/54/146541041_ccdf28d640_o.jpg" class="thumb" rel="lightbox" title="Dublin Coast"><img src="http://farm1.static.flickr.com/54/146541041_ccdf28d640_s.jpg" alt="Dublin Coast" /></a>
<a href="http://farm1.static.flickr.com/50/146540504_c4ef35d370_o.jpg" class="thumb" rel="lightbox" title="Bulmers. Cheers Mate!"><img src="http://farm1.static.flickr.com/50/146540504_c4ef35d370_s.jpg" alt="Bulmers. Cheers Mate!" /></a>
<a href="http://farm1.static.flickr.com/55/146540016_2532ca7263_o.jpg" class="thumb" rel="lightbox" title="Central Dublin"><img src="http://farm1.static.flickr.com/55/146540016_2532ca7263_s.jpg" alt="Central Dublin" /></a>
<a href="http://farm1.static.flickr.com/52/146539726_2530fe446b_o.jpg" class="thumb" rel="lightbox" title="Guiness for Strength"><img src="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.

function lightbox() {
    var links = $( 'a[rel^=lightbox]' );
    var overlay = $( jQuery( '<div id="overlay" style="display: none"></div>' ) );
    var container = $( jQuery( '<div id="lightbox" style="display: none"></div>' ) );
    var target = $( jQuery( '<div class="target"></div>' ) );
    var close = $( jQuery( '<a href="#close" class="close">&times; Close</a>' ) );
    var prev = $( jQuery( '<a href="#prev" class="prev">&laquo; Previous</a>' ) );
    var next = $( 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:

var overlay = 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();
    var current = parseInt( links.filter( '.selected' ).attr( 'lb-position' ),10 );
    var to = $( 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 ) {
    var link = $( 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

var open = 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

var loadImage = function( url ) {
    if( container.is( '.loading' ) ) { return; }
    container.addClass( 'loading' );
    var img = new Image();
    img.onload = function() {
        img.style.display = 'none';
        var maxWidth = ( $( window ).width() - parseInt( container.css( 'padding-left' ),10 ) - parseInt( container.css( 'padding-right' ), 10 ) ) - 100;
        var maxHeight = ( $( 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 window
            var ratio = 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.

Comments (7)

  1. nzy on Sept. 4, 2009

    Very good article.. Thanks. BTW, the light box in this page does not work in IE 8 (I only have IE 8), though the example you provided "http://www.huddletogether.com/projects/lightbox/ " do.. Is there anything that we can change to make compatible with IE?

  2. Daniel Ryan on Sept. 4, 2009

    I'll look into it. Have to love old IE.

  3. zy wh on Sept. 8, 2009

    Hello sir,
    I've changed a little bit in the CSS file(lightbox-demo.css), i only put background setting for firefox here.

    #overlay {display: none; position: fixed; _position: absolute; top: 0; left: 0; width: 100%; height:100%; background: rgba(255,255,255,0.8); }

    and in "lightbox-demo.js" I changed in the following like to make the background working for IE . In original code it does not have any style values except "display:none".

    var overlay = $(jQuery('&lt;div id="overlay" style="display: none; position: fixed; _position: absolute; top: 0; left: 0; width: 100%; height:100%; background-color: #CCCCFF; filter:alpha(opacity=40); "&gt;&lt;/div&gt;'));

    Now everything is ok in both IE and Firefox. But I don't know exactly why this need to be done.

  4. Gene on Jan. 6, 2010

    I use Firefox all the time, but I noticed this page throws a javascript error in IE8

  5. ngocviet on June 11, 2010

    I cannot download the script and css file.
    Please upload them again.
    Thanks.

  6. timo huovinen on July 1, 2010

    nice article, but I did not catch how the semi-transparent background is done?

  7. Daniel Ryan on July 1, 2010

    It's all in the CSS, using rgba for modern browsers and an filter rule for IE.

Leave a Comment

Please log in to post a comment.

Summary

One of the most popular user interfaces in recent years is the lightbox concept for viewing images. Learning how to build one makes a great introduction to jQuery as well.

Sharing

About the Author