1 ? " Photos" : " Photo")).appendTo(item); item.appendTo("#content"); //add the item to the content area }); }; The first thing that we’ll want to do, is remove everything from the content area, and remove the back button if it is there. This clears the way for us to render the album view. So that we have an easier time styling the album view in our CSS, we add the album_view class to the content area. Then we hide the footer and extent the content area into the empty space, remove the view class from the header, and set the currently selected photo to the first one. Much of this is only necessary when we are returning to album view from another view, and not when we first load. Now, we can add the content of the album view to the content area. The first thing we add, is the blue label that says “Albums”. Next we add each of the albums, using the template that we defined above. I used $.each to do this, which loops through the albums array (from our data file), and calls the function that I’ve passed to it with each of the albums. The first thing we will create is the wrapper, which has a click handler assigned to it. When you click on an album, it sets the data, title, and image sprite of the album that you clicked on, and then renders the selected view (which when you first load is the grid view). The next thing we’ll create is the skimming effect, which is actually fairly easy since we are using CSS sprites. We create the <div class=”skimmer”>, and set it’s background image to be the image sprite from the current album (this refers the current album object since we are using jQuery’s each function). Next we will register a mousemove handler. This is where the action happens. We set the variable x to the current mouse position relative to the whole page, not the album element. To remedy this, we find the offset of the current album element (this), from the left of the window. We are lucky that jQuery provides a nice function to do just that! The w variable, is equal to the width of the album element (160), divided by the number of photos in the album. This calculates the width of each of the sections of the skimmer, and will allow us to calculate the image to display. The image to display is calculated by first subtracting the offset from x which gives us the current mouse position relative to the album element, and dividing it by the width of the sections. This will give us the index of the image we want to display. Since we are using CSS sprites, we just have to set the background position on the Y axis (since the photos in this sprite are stacked on top of each other) to negative (because we are moving down) 160 (the height of the skimmer), multiplied by the current image. This will move the current background position down to the image to be displayed. The last thing for the album view is to create the CSS, which is below. .album_view { font-size: 16px !important; } .album_view h2 { color: rgb(96, 121, 149); font: 21px 'Helvetica Neue', Helvetica, Arial, sans-serif; text-shadow: black 0px 2px 3px; } .album_view .item { width: 160px; height: 200px; cursor: pointer; float: left; margin-right: 50px; white-space: nowrap; } .album_view .item .skimmer { width: 160px; height: 160px; -webkit-border-radius: 8px; -moz-border-radius: 8px; } .album_view .item strong { color: rgb(191, 191, 191); font: bold 15px 'Helvetica Neue', Helvetica, Arial, sans-serif; text-shadow: black 0px 0px 2px; display: block; -webkit-transition: all 500ms; overflow: hidden; text-overflow: ellipsis; } .album_view .item span { color: rgb(102, 102, 102); font: bold 11px 'Helvetica Neue', Helvetica, Arial, sans-serif; text-shadow: black 0px 0px 2px; -webkit-transition: color 500ms; } .album_view .item:hover strong { color: white; } .album_view .item:hover span { color: rgb(112, 176, 255); } We only set the font size of the album view because of the grid view which we will discus next, but the rest is fairly basic. One cool thing that I’ve used (again, only visible in webkit based browsers), are the new CSS transitions. This way, when you hover over an album, the labels for the title of the album, and the number of photos fade in nicely rather than changing color instantly. This is a nice effect, and is really easy to implement. I hope other browsers adopt it soon. Now we have a working album view, which should look like this: Step 6: The Grid View As mentioned before, there are several views that the user can switch between, which visualize the photos inside an album in different ways. For this tutorial, I’ve implemented two of them: grid view, and mosaic view. We will start with grid view. Apple does the layout for the Grid View entirely with JavaScript. Each image is absolutely positioned, and when you resize your window, or drag the size slider in the bottom right, the JavaScript needs to recalculate the position and size of all of the images. This takes a lot more code than the pure CSS method that we will use. We will start with the jQuery. In order to make the grid scalable with the slider, we will convert the size of the images to ems. Since one em is equal to the font-size, the slider will simply adjust the font-size of the content area, thus scaling the images. The slider has already been set up in the main jQuery function. I saw this technique recently in a Web Design Ledger article, so if you want a more detailed explanation, you should read that. function gridView() { //remove everything from the content area that might have been there from other views, //add the grid_view class to the content view, and set up the title bar $("#content *").remove(); $("#content").attr("class", "").addClass("grid_view"); $("h1").addClass("view").html(title).show(); $(".button").remove(); //set up the footer view, and the content area $("#controls").show(); $("#controls #slider").show(); $("#content").css({ bottom: "40px", top: "57px" }); view = gridView; //set the current view //add the back button $('<div class="button">').html(gallery).click(function() { albumView(); //go back to the current view }).appendTo("body"); //add the items in the album to the grid, and set the size of the image in ems. $.each(data, function(i) { var item = $('<div class="grid_item">').click(function() { //largeView(this, i); }); $('<img/>').attr("src", this.src) .css("width", this.width / 100 + "em") .css("height", this.height / 100 + "em") .appendTo(item); $("<strong/>").html(this.title).appendTo(item); item.appendTo("#content"); }); }; As with the album view, we first need to clear our canvas so that we can render our new view. I won’t go into as much detail as the album view, since most of it is the same, but there are two different things: this time we show the controls (along with the slider), and set the main content area to have a bottom of 40px, or the height of the footer. We also set the current view to grid view, and add the back button which will return us to album view. The meat of the grid view, is again in $.each, but this time we will not be going through the albums, but going through the data variable that we defined at the beginning, and set to hold the current album’s photos when we clicked on an album in album view. For each photo, we will create a div with the class of grid_item, and if we had a large view for the photos like Apple does, we would call it when the item was clicked on. As you can see, this is commented out because we will not be implementing the large view in this tutorial. To the grid item, we will add the current image with it’s width and height set to the size that we gave in our data file divided by 100 in order to convert it ems. This will allow easy scaling of the images by our slider. We also add a label with the title of the image to the grid item. To complete our Grid View, we need some CSS. One interesting thing that we will do, is set a background color for the images. This means that while the images are loading, we will see a gray color (or if you are running webkit, a pretty gradient). You will also notice, that throughout the gallery interface, I’ve been using some webkit specific CSS properties, including gradients and reflections. This makes the interface look a little better in webkit browsers, but it still looks decent in other browsers since I used fallback styles. Again, some of the CSS is explained in the Web Design Ledger article. .grid_view { font-size: 75px; overflow: auto; } .grid_item { float: left; margin: 0 0.5em 1.2em 0; cursor: pointer; height:2em; width: 2.5em; line-height: 3em; text-align: center; } .grid_item strong { color: rgb(170, 170, 170); font: bold 13px 'Helvetica Neue', Helvetica, Arial, sans-serif; text-shadow: black 0px 0px 2px; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; -webkit-transition: color 500ms; } .grid_item:hover strong { color: white; } .grid_item img { vertical-align: bottom; background: rgb(30, 30, 30); background: -webkit-gradient(linear, left top, left bottom, from(rgb(50, 50, 50)), to(rgb(10, 10, 10))); -webkit-box-reflect:below 1px -webkit-gradient(linear, left top, left 130%, from(transparent), color-stop(0.5, transparent), to(rgba(255, 255, 255, 0.5))); } Again, the CSS is pretty self explanatory, but one thing to note is that we set an initial font size of 75px which matches up with the initial value of the slider. We also set the label of the grid item, to have text-overflow: ellipsis, which means that if the width of the grid item becomes too small to fit the text in, three dots will appear at the end of the text to show that there is more. Again, this is a nice little touch, but it doesn’t work in Firefox. We should now have a Grid View that looks something like this. Step 7: The Mosaic View My favorite view in Apple’s Web Gallery, is the Mosaic View. It displays a large view of the selected image on the left, and a grid of thumbnails on the right. When you resize your window, the whole interface resizes, so that the entire large view is always visible, and the thumbnail grid changes the number of columns that it displays. The only part that will ever need to scroll, is the grid on the right since the large image resizes to fit the window. Apple also implements this view with JavaScript, setting an absolute position for each of the thumbnails, and reflowing the entire interface when you resize. We will recreate the interface with pure CSS, but first we need to generate our content with a bit of jQuery. function mosaicView() { //remove everything from the content area that might have been there from other views, //add the grid_view class to the content view, and set up the title bar $("#content *").remove(); $("#content").attr("class", "").addClass("mosaic_view"); $("h1").addClass("view").html(title).show(); $(".button").remove(); //set up the footer view, and the content area $("#controls #slider").hide(); $("#controls").show(); $("#content").css({ bottom: "40px", top: "57px" }); view = mosaicView; //set the current view //add the back button $('<div class="button">').html(gallery).click(function() { albumView(); //go back to the current view }).appendTo("body"); //add the large view with title var detail = $('<div id="mosaic_detail">').click(function(i) { //largeView(this, current); }); $("<img/>").attr("src", data[current].src).appendTo(detail); $("<strong/>").html(data[current].title).appendTo(detail); detail.appendTo("#content"); //add the thubnail grid, with a click handler to animate the image change var grid = $('<div id="mosaic_grid">'); $.each(data, function(i) { $('<div class="mosaic_item">') .css({ backgroundPosition: "0px " + (-160 * i) + "px", backgroundImage: "url(" + sprite + ")" }) .data("num", i) .click(function() { var num = $(this).data("num"); current = num; $(".mosaic_item.selected").removeClass("selected"); $(this).addClass("selected"); $("#mosaic_detail").animate({ opacity: 0 }, "fast", function() { $("#mosaic_detail img").attr("src", data[num].src); $("#mosaic_detail strong").html(data[num].title); $(this).animate({ opacity: 1 }, "fast"); }); }).appendTo(grid); }); grid.appendTo("#content"); //select the current item in the thumbnail grid view $(".mosaic_item:nth-child("+ (current + 1) +")").addClass("selected"); }; Once again, we do our usual reset at the beginning, and set the current view to mosaic view. The first thing that we create, is the large image on the left which again, if we had it, would trigger Apple’s large view. To hold this image, we create a div with the class of “mosaic_detail”, and add the currently selected image to it, along with the title label. The next thing we create, is the grid on the right. This happens once again in a $.each function. For each image in the album, we create a div with the “mosaic_item” class which will be added the parent div with the class of “mosaic_grid”. This item, will use the same CSS sprite that we used in our album view, in order to save on the number of images we need to download. We will set the background position based on the variable i, which is passed into the $.each function, and holds the index of the current image. Next, we use jQuery’s data function to store the index of the current image within the DOM element itself, and create a click handler to set the currently selected image. We use the data function to retrieve the index from the DOM element when it is clicked on, and set the global variable current to equal that index. Next, since we want to style the currently selected grid item in our CSS, we set the selected class to the item we clicked on, and remove the class from the previous selection if any. Finally, we animate the large view on the left to have an opacity of 0. This makes it invisible for a short period of time, while we change the image and title to the new selection. Then we animate back to visible. This creates a nice smooth effect when we change the selection. Now that we have the content and interactivity set up, we can create the layout with our CSS. In order to make the layout flexible, we will use percentages and the max-width property. The diagram below shows the layout that we will create. As you can see in the diagram, the large image on the left has a maximum height of 97%. This means that the height of the image will never exceed 97% of the height of the content area, but the current height is calculated so that the image remains proportional. The maximum width of the large image is set to 62%, and the grid view on the right has a left position of 64%, which creates a little space between the large image, and the grid. The grid has it’s width set to auto, which makes it extend to the edge of the browser window if necessary. It also has it’s overflow property set to auto, so that if there are more images in the grid than can fit in the given space, vertical scrollbars will appear. I’ve also set the minimum width of the grid, so that there will never be less than two columns. A minimum height on the large image is also set so that things don’t get too small, and the user knows to make their window larger. When a grid item becomes selected, the jQuery adds the selected class to the item, so that we can style it. The only thing we do with this, is to use the outline property to make a gray border around the image. We also make a thinner outline when the grid item is hovered over. The CSS for creating this layout, as well as some styling to make it look nice, is below. .mosaic_view { overflow: hidden !important; padding-top: 30px; min-width: 700px; } #mosaic_detail { width: 62%; min-width: 435px; height: 95%; cursor: pointer; } #mosaic_detail img { display: block; margin: auto; max-width: 100%; max-height: 97%; min-height: 212px; -webkit-box-reflect:below 1px -webkit-gradient(linear, left top, left 130%, from(transparent), color-stop(0.5, transparent), to(rgba(255, 255, 255, 0.4))); } #mosaic_detail strong { color: rgb(170, 170, 170); font: bold 13px 'Helvetica Neue', Helvetica, Arial, sans-serif; text-align: center; padding-bottom: 50px; padding-top: 5px; display: block; -webkit-transition: color 500ms; } #mosaic_detail:hover strong { color: white; } #mosaic_grid { position: absolute; left: 64%; top: 0px; bottom: 0px; width: auto; padding-top: 20px; /* 30 - 10 */ padding-left: 2px; min-width: 250px; overflow: auto; } .mosaic_item { width: 95px; height: 95px; float: left; margin: 10px 10px 0 0; border: 1px solid black; cursor: pointer; } .mosaic_item:hover { outline: 1px solid rgb(170, 170, 170); } .mosaic_item.selected { outline: 2px solid rgb(170, 170, 170); } We should now have a flexible interface for our Mosaic View, that looks something like the screenshot below. You can see that the image on the left fits in it’s space, doesn’t overflow into other parts of the page, and remains proportional. Conclusion and Disclaimers Phew! We have finally come to the end! We should now have a fairly usable image gallery, and it is up to you to add features to it. If you connected it to a database, and wrote a nice back end to upload photos, you would be pretty close to having a complete product. Now on to the disclaimers: If you try the demo in Internet Explorer (any version), it does not look how it was intended. I did not try to make it work in IE, but if you would like to do so, feel free (and post your fixes in the comments!). I did not try to optimize the interface for load time (such as providing smaller images), so you may experience slowness the first time you load the images. This demo was purely to show how to create the interface itself. I intentionally left out three of the views that Apple has for the sake of space: A larger view when you click on an image in the grid view or the large image in the mosaic view (which is commented out in the code), the slideshow view, and the carousel view. All of the images used in the demo were taken from Apple’s demo MobileMe gallery. If you have any questions or comments, please leave them here or send me a message on Twitter. I hope you enjoyed the tutorial! Follow us on Twitter, or subscribe to the NETTUTS RSS Feed for more daily web development tuts and articles." />

Recreating the MobileMe Web Gallery Interface

In 2007, Apple updated .Mac (now MobileMe), and introduced the “Web Gallery” feature - a photo sharing service that integrates with iPhoto. Web Gallery offers a beautiful interface, but the layout is done entirely with JavaScript. In this tutorial, we will attempt to recreate the Web Gallery interface using only CSS and a small amount of jQuery.


Preface

We will be aiming to make our page look something like this.

You will need the following for this tutorial:

Final Product

Organize the Files

Let’s start by organizing our files. First create a file called index.html in the root of your project folder. Next create a folder called assets, inside of which you’ll need three folders: css, data, and js. In the css folder, create a file called styles.css, in the data folder create a file called data.json, and in the js folder create a file called JavaScript.js, and place the downloaded jQuery and jQuery UI files in the js folder as well. You don’t have to use the same folder structure that I do, but if you decide not to, remember to modify the paths that we use in the code!

Step 1: The HTML

We will use very basic HTML for this tutorial since most of the code will be generated by jQuery. The Web Gallery interface has a three major sections - A header, a body, and a footer - some of which are hidden at certain times.

The <h1> tag is the header, the #content div is the body section, and the #controls div is the footer. I’m going to use the HTML5 doctype, but you can use whichever you’d like!

<!DOCTYPE html>
<html>
    <head>
        <title>Gallery</title>
        <link rel="stylesheet" type="text/css" href="assets/css/styles.css">
        <script type="text/JavaScript" src="assets/data/data.json"></script>
        <script type="text/JavaScript" src="assets/js/jquery-1.3.2.min.js"></script>
        <script type="text/JavaScript" src="assets/js/jquery-ui-1.7.1.custom.min.js"></script>
        <script type="text/JavaScript" src="assets/js/JavaScript.js"></script>
     </head>
     <body>
         <h1>Devon's Gallery</h1>
         <div id="content">

         </div>
         <div id="controls">
             <div id="views">
                 <a href="#" id="grid" class="selected">Grid</a>
                 <a href="#" id="mosaic">Mosaic</a>
             </div>
             <div id="slider"></div>
         </div>

     </body>
</html>

We bring in 5 external files in the <head> section of our HTML: first is our css file, second is a json file containing our data (discussed in the next section), next we have the jQuery and jQuery UI JavaScript files, and last is our yet to be written JavaScript file.

Step 2: The Data

The data.json file holds our data for this demo. For the purpose of simplicity, I’ve made the data for the tutorial static, but it would be pretty easy to connect it to a database. The JSON format allows for easy parsing by our JavaScript.

var gallery = "My Great Gallery";
var albums = [
    {
        title: "Lake Tahoe",
        sprite: "http://gallery.me.com/emily_parker/100579/scrubSprite.jpg?ver=121513594900013",
        photos: [
            {
                title: "On the river",
                src: "http://gallery.me.com/emily_parker/100579/Lake-20Tahoe-201/web.jpg?ver=12151358310001",
                width: 260,
                height: 192
            },
            {
                title: "Mike and Nancy",
                src: "http://gallery.me.com/emily_parker/100579/Lake-20Tahoe-202/web.jpg?ver=12151358290001",
                width: 260,
                height: 192
            },
            {
                title: "Carrying the canoe",
                src: "http://gallery.me.com/emily_parker/100579/Lake-20Tahoe-203/web.jpg?ver=12151358330001",
                width: 260,
                height: 192
            },
            {
                title: "In the tent",
                src: "http://gallery.me.com/emily_parker/100579/Lake-20Tahoe-204/web.jpg?ver=12151358290001",
                width: 260,
                height: 192
            },
            {
                title: "Starting a laugh",
                src: "http://gallery.me.com/emily_parker/100579/Lake-20Tahoe-205/web.jpg?ver=12151358330001",
                width: 260,
                height: 192
            },
            {
                title: "The whole gang",
                src: "http://gallery.me.com/emily_parker/100579/Lake-20Tahoe-206/web.jpg?ver=12151358300001",
                width: 260,
                height: 192
            },
            {
                title: "Paddling downstream",
                src: "http://gallery.me.com/emily_parker/100579/Lake-20Tahoe-207/web.jpg?ver=12151358280001",
                width: 260,
                height: 192
            },
            {
                title: "Carla and Sarah",
                src: "http://gallery.me.com/emily_parker/100579/Lake-20Tahoe-208/web.jpg?ver=12151358320001",
                width: 260,
                height: 192
            },
            {
                title: "No shoes required",
                src: "http://gallery.me.com/emily_parker/100579/Lake-20Tahoe-209/web.jpg?ver=12151358310001",
                width: 260,
                height: 192
            },
            {
                title: "Nancy",
                src: "http://gallery.me.com/emily_parker/100579/Lake-20Tahoe-2010/web.jpg?ver=12151358280001",
                width: 260,
                height: 192
            },
            {
                title: "Getting ready to float",
                src: "http://gallery.me.com/emily_parker/100579/Lake-20Tahoe-2011/web.jpg?ver=12151358320001",
                width: 260,
                height: 192
            }
        ]
    }
    //Add more albums here...
];

The first thing you see in this file, is a variable called gallery. This holds the title of the whole gallery that you’ll see displayed in the header when the gallery first loads. The rest of the file is an array called albums, which holds each of the albums that we might want to display in our gallery. Each album has a title, an image sprite (which we’ll get to in a minute), and an array of photo objects, each of which have a title, url, width, and height.

Step 3: The Beginnings of our CSS

Next, we need to add the CSS that will control the general layout of the page.

body {
    margin: 0;
    padding: 0;
    background: black;
    overflow: hidden;
    height:100%;
    border:0;
}

h1 {
    background: rgb(40, 40, 40);
    background: -webkit-gradient(linear, left top, left bottom, from(rgb(75, 75, 75)), to(rgb(13, 13, 13)));
    color: white;
    font: 29px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    text-shadow: rgb(0, 0, 0) 0px -2px 0px;
    line-height: 57px;
    padding-left: 26px;
    margin: 0;
    border-bottom: 1px solid rgb(31, 31, 31);
    position:absolute;
    top:0;
    left:0;
    display:block;
    width:100%;
    height:57px;
    z-index:5;
    overflow:hidden;
    margin:0;
}

h1.view {
    text-align: center;
    font-size: 24px;
    text-shadow: rgb(0, 0, 0) 0px -1px 1px;
    padding: 0px;
}

.button {
    background: rgb(29, 29, 29);
    background: -webkit-gradient(linear, left top, left bottom, from(rgb(96, 96, 96)), color-stop(0.5, rgb(9, 9, 9)), to(rgb(29, 29, 29)));
    border: 1px solid rgb(35, 35, 35);
    border-top-color: rgb(55, 55, 55);
    -webkit-border-radius: 3px;
    -moz-border-radius: 3px;
    height: 24px;
    color: rgb(224, 224, 224);
    font: bold 13px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    padding: 5px 10px 0 10px;
    display: inline-block;
    text-shadow: rgb(30, 30, 30) 0px 0px 2px;
    -webkit-user-select: none;
    -moz-user-select: none;
    cursor: default;
    position: absolute;
    top: 13px;
    left: 26px;
    z-index: 5;
}

.button:active {
    background: rgb(12, 12, 12);
    background: -webkit-gradient(linear, left top, left bottom, from(rgb(68, 68, 68)), color-stop(0.5, rgb(7, 7, 7)), to(rgb(12, 12, 12)));

}

#content {
    padding-left: 26px;
    overflow:auto;
    position:absolute;
    z-index:3;
    top:57px;
    bottom:0px;
    left:0px;
    right: 0px;
}

/*
    Controls
*/

#controls {
    background: rgb(51, 51, 51);
    background: -webkit-gradient(linear, left top, left bottom, from(rgb(30, 30, 30)), color-stop(0.2, rgb(51, 51, 51)));
    display: none;
    position:absolute;
    margin:0;
    bottom:0;
    left:0;
    width:100%;
    height:40px;
    z-index:5;
    overflow:hidden;
}

#slider {
    position: absolute;
    top: 18px;
    right: 20px;
    width: 137px;
    height: 15px;
    background: url(http://gallery.me.com/g/images/gallery/view_control/track_fill_left.png) no-repeat;
    border: none;
    -webkit-border-radius: 0px;
}

#slider .ui-slider-handle {
    background: url(http://gallery.me.com/g/images/gallery/view_control/knob.png) no-repeat;
    width: 16px;
    height: 16px;
    outline: none;
    position: absolute;
    top: -6px;
    margin-left: -6px;
    z-index: 2;
    cursor: default;
}

.ui-slider {
    position: relative;
    text-align: left;
}

#views {
    position: absolute;
    top: 12px;
    left: 20px;
}

#views a {
    color: rgb(97, 97, 97);
    text-decoration: none;
    font: bold 12px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    text-shadow: rgb(17, 17, 17) 0px -1px 1px;
    margin-right: 17px;
    padding-left: 20px;
    display: inline-block;
    cursor: pointer;
    background: url(http://gallery.me.com/g/images/gallery/view_control/view_sprite.png) no-repeat;
}

#views #mosaic {
    background-position: 0px -20px;
}

#views #grid:hover, #views #grid.selected {
    color: white;
    background-position: 0px -80px;
}

#views #mosaic:hover, #views #mosaic.selected {
    color: white;
    background-position: 0px -100px;
}

Whew! That was a lot of CSS. Let me explain a little. The first really important line comes in the body selector, and it is overflow: hidden. This will help with our layout, since we only want the body section to scroll, leaving the header and footer fixed. The header is set to position: absolute, with its top and left positions set to 0. It is also set to be a block element with a width of 100%, which will make it extend all the way across the window. I’ve also used a gradient background, which only webkit users will see at this point, but which makes it look a little nicer. There is a second selector for the header, this time with the class of view. The view class will be added to the header, when we viewing the contents of an album.

Next we style the button, which will appear while viewing the contents of an album, and will bring us back to the list of albums. I also used a webkit-gradient for the background here, but anytime I do that, I have made sure to provide a backup color which will display in browsers that don’t support the gradients. Another thing that I did, was use the -webkit-user-select property, and Firefox’s respective -moz-user-select property (from CSS3), which causes selection of the text in the button to be disabled. An active state with a darker background is also provided, so that when you mouse down on the button it looks selected.

The #content area is styled next. This is the section that we wanted to scroll, so we use overflow: hidden. After setting it to position: absolute, we set the top, bottom, left and right properties which many people do not know that you can do. The top is set to 57px, which is the height of the header, and the bottom is set to 0px for the main page since the footer will not be visible until we are viewing the contents of an album. When we are doing this, the bottom will be switched to the height of the footer (40px) by the jQuery. Both the left and right positions are set to 0px, so that the content area automatically extends the full width of the page.

The last section to be styled, is the #controls section, or footer and all of it’s children. We again use position: absolute, and set bottom: 0 so that it sticks to the bottom of the window. As stated earlier, the footer is not visible when the page first loads, so we set display: none. We will be using the slider view from jQuery UI, so the next thing we style is track, and handle for that. The final thing that we style, are the #views. These are the links on the left side of the footer, which switch between the galleries different display modes. We will have two of these in our re-creation: #grid, and #mosaic, which we will be creating next.

Step 4: The Beginnings of the jQuery

Now that we’ve set up the basics of our page layout, we can get down to some JavaScript. Let’s jump right in!

var current = 0; //The currently selected image
var view; //The currently selected view
var data = []; //The data for the currently selected album
var title = ""; //The title of the currently selected album
var sprite = ""; //The sprite of the currently selected album
var gridView, mosaicView; //the views, defined later

$(document).ready(function() {
    view = gridView; //this referrs to a function that we havn't created yet
    albumView(); //render the album view

    //set up the jQuery UI slider for later use by the grid view
    $("#slider").slider({
    	value: 75,
    	max: 150,
    	min: 50,
    	slide: function(event, ui) {
    	    $('#content').css('font-size',ui.value+"px");
    	}
    });

    //setup the buttons for switching views
    $("#grid").click(function() {
        $("#views a.selected").removeClass("selected");
        $(this).addClass("selected");
        gridView();
    });

    $("#mosaic").click(function() {
        $("#views a.selected").removeClass("selected");
        $(this).addClass("selected");
        mosaicView();
    });

});

The first thing that we’ll do, is to set up some global variables to be used later. These will keep track of the currently selected image, the currently selected view, the data, title, and image sprite of the currently selected album, and variables to be defined later which will hold our two views. The next thing that we’ll do, is to use jQuery’s $(document).ready() function, which automatically runs when the DOM is ready for modification. First, we’ll set the currently selected view to gridView, which we haven’t defined yet. Next we will call the albumView() function, which will render the home screen. Finally, we will set up the jQuery UI slider (which I will explain in a minute), and the click handlers for the view links in the footer, which will change the currently selected view.

Step 5: The Album View

The album view does a neat thing that Apple calls skimming. As you move your mouse over the albums, you see each photo in the album you are hovering over, depending on the position of your mouse. You can see the desired effect in Apple’s version here.

Our jQuery will generate the HTML for each album from the data, based on the following template:

<div class="item">
	<div class="skimmer">
		<!-- this div has a background image -->
	</div>
	<strong>Album Title</strong>
	<span>Number of Photos</span>
</div>

The <div class=”item”>, is just a wrapper for each album, within which there is a <div class=”skimmer”> which will have a background image set to the image sprite of the album, the album title, and number of photos.

The jQuery for generating the album view is below.

function albumView() {
    //remove everything from the content area that might have been there from other views,
    //as well as the back button (used later)
    $("#content *").remove();
    $(".button").remove();

    //add the album_view class to the content view, and extent it to the bottom of the window
    //also, hide the footer, and set the title of the gallery (in the data file)
    $("#content").attr("class", "").addClass("album_view");
    $("#controls").hide();
    $("#content").css({ bottom: "0px", top: "57px" });
    $("h1").removeClass("view").html(gallery);
    current = 0; 

    $("<h2>Albums</h2>").appendTo("#content"); //add the title of the view
    $.each(albums, function(i) {
    	//create the album, and register the click handler
        var item = $('<div class="item">').click(function() {
            data = albums[i].photos;
            title = albums[i].title;
            sprite = albums[i].sprite;
            view(); //go to current view
        });

        //create the skimmer, and set the background image to the sprite for the album (in the data file)
        //then register the mousemove event
        $('<div class="skimmer">').css("background", "url("+this.sprite+")").mousemove(function(e) {
            var x = e.pageX;
            var offset = $(this).offset().left;
            var w = 160 / albums[i].photos.length;
            var image = Math.floor((x - offset) / w);
            $(this).css("background-position", "0px " + (-160 * image) + "px");
        }).mouseout(function() {
        	//when we mouseout, set the background position back to 0
            $(this).css("background-position", "0px 0px");
        }).appendTo(item);

        //create the album title and the number of photos label
        $('<strong/>').html(this.title).appendTo(item);
        $('<span/>').html(this.photos.length + (this.photos.length > 1 ? " Photos" : " Photo")).appendTo(item);

        item.appendTo("#content"); //add the item to the content area
    });
};

The first thing that we’ll want to do, is remove everything from the content area, and remove the back button if it is there. This clears the way for us to render the album view. So that we have an easier time styling the album view in our CSS, we add the album_view class to the content area. Then we hide the footer and extent the content area into the empty space, remove the view class from the header, and set the currently selected photo to the first one. Much of this is only necessary when we are returning to album view from another view, and not when we first load.

Now, we can add the content of the album view to the content area. The first thing we add, is the blue label that says “Albums”. Next we add each of the albums, using the template that we defined above. I used $.each to do this, which loops through the albums array (from our data file), and calls the function that I’ve passed to it with each of the albums. The first thing we will create is the wrapper, which has a click handler assigned to it. When you click on an album, it sets the data, title, and image sprite of the album that you clicked on, and then renders the selected view (which when you first load is the grid view).

The next thing we’ll create is the skimming effect, which is actually fairly easy since we are using CSS sprites. We create the <div class=”skimmer”>, and set it’s background image to be the image sprite from the current album (this refers the current album object since we are using jQuery’s each function). Next we will register a mousemove handler. This is where the action happens. We set the variable x to the current mouse position relative to the whole page, not the album element. To remedy this, we find the offset of the current album element (this), from the left of the window. We are lucky that jQuery provides a nice function to do just that! The w variable, is equal to the width of the album element (160), divided by the number of photos in the album. This calculates the width of each of the sections of the skimmer, and will allow us to calculate the image to display. The image to display is calculated by first subtracting the offset from x which gives us the current mouse position relative to the album element, and dividing it by the width of the sections. This will give us the index of the image we want to display. Since we are using CSS sprites, we just have to set the background position on the Y axis (since the photos in this sprite are stacked on top of each other) to negative (because we are moving down) 160 (the height of the skimmer), multiplied by the current image. This will move the current background position down to the image to be displayed.

The last thing for the album view is to create the CSS, which is below.

.album_view {
	font-size: 16px !important;
}

.album_view h2 {
	color: rgb(96, 121, 149);
	font: 21px 'Helvetica Neue', Helvetica, Arial, sans-serif;
	text-shadow: black 0px 2px 3px;
}

.album_view  .item {
    width: 160px;
    height: 200px;
    cursor: pointer;
    float: left;
    margin-right: 50px;
    white-space: nowrap;
}

.album_view .item .skimmer {
    width: 160px;
    height: 160px;
    -webkit-border-radius: 8px;
    -moz-border-radius: 8px;
}

.album_view .item strong {
    color: rgb(191, 191, 191);
    font: bold 15px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    text-shadow: black 0px 0px 2px;
    display: block;
    -webkit-transition: all 500ms;
    overflow: hidden;
    text-overflow: ellipsis;
}

.album_view .item span {
    color: rgb(102, 102, 102);
    font: bold 11px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    text-shadow: black 0px 0px 2px;
    -webkit-transition: color 500ms;
}

.album_view .item:hover strong {
    color: white;
}

.album_view .item:hover span {
    color: rgb(112, 176, 255);
}

We only set the font size of the album view because of the grid view which we will discus next, but the rest is fairly basic. One cool thing that I’ve used (again, only visible in webkit based browsers), are the new CSS transitions. This way, when you hover over an album, the labels for the title of the album, and the number of photos fade in nicely rather than changing color instantly. This is a nice effect, and is really easy to implement. I hope other browsers adopt it soon.

Now we have a working album view, which should look like this:

Step 6: The Grid View

As mentioned before, there are several views that the user can switch between, which visualize the photos inside an album in different ways. For this tutorial, I’ve implemented two of them: grid view, and mosaic view. We will start with grid view.

Apple does the layout for the Grid View entirely with JavaScript. Each image is absolutely positioned, and when you resize your window, or drag the size slider in the bottom right, the JavaScript needs to recalculate the position and size of all of the images. This takes a lot more code than the pure CSS method that we will use.

We will start with the jQuery. In order to make the grid scalable with the slider, we will convert the size of the images to ems. Since one em is equal to the font-size, the slider will simply adjust the font-size of the content area, thus scaling the images. The slider has already been set up in the main jQuery function. I saw this technique recently in a Web Design Ledger article, so if you want a more detailed explanation, you should read that.

function gridView() {
    //remove everything from the content area that might have been there from other views,
    //add the grid_view class to the content view, and set up the title bar
    $("#content *").remove();
    $("#content").attr("class", "").addClass("grid_view");
    $("h1").addClass("view").html(title).show();
    $(".button").remove();  

    //set up the footer view, and the content area
    $("#controls").show();
    $("#controls #slider").show();
    $("#content").css({ bottom: "40px", top: "57px" });

    view = gridView; //set the current view

    //add the back button
    $('<div class="button">').html(gallery).click(function() {
        albumView(); //go back to the current view
    }).appendTo("body");

    //add the items in the album to the grid, and set the size of the image in ems.
    $.each(data, function(i) {
        var item = $('<div class="grid_item">').click(function() {
            //largeView(this, i);
        });
        $('<img/>').attr("src", this.src)
                   .css("width", this.width / 100 + "em")
                   .css("height", this.height / 100 + "em")
                   .appendTo(item);
        $("<strong/>").html(this.title).appendTo(item);
        item.appendTo("#content");
    });
};

As with the album view, we first need to clear our canvas so that we can render our new view. I won’t go into as much detail as the album view, since most of it is the same, but there are two different things: this time we show the controls (along with the slider), and set the main content area to have a bottom of 40px, or the height of the footer. We also set the current view to grid view, and add the back button which will return us to album view.

The meat of the grid view, is again in $.each, but this time we will not be going through the albums, but going through the data variable that we defined at the beginning, and set to hold the current album’s photos when we clicked on an album in album view. For each photo, we will create a div with the class of grid_item, and if we had a large view for the photos like Apple does, we would call it when the item was clicked on. As you can see, this is commented out because we will not be implementing the large view in this tutorial. To the grid item, we will add the current image with it’s width and height set to the size that we gave in our data file divided by 100 in order to convert it ems. This will allow easy scaling of the images by our slider. We also add a label with the title of the image to the grid item.

To complete our Grid View, we need some CSS. One interesting thing that we will do, is set a background color for the images. This means that while the images are loading, we will see a gray color (or if you are running webkit, a pretty gradient). You will also notice, that throughout the gallery interface, I’ve been using some webkit specific CSS properties, including gradients and reflections. This makes the interface look a little better in webkit browsers, but it still looks decent in other browsers since I used fallback styles. Again, some of the CSS is explained in the Web Design Ledger article.

.grid_view {
    font-size: 75px;
    overflow: auto;
}

.grid_item  {
    float: left;
    margin: 0 0.5em 1.2em 0;
    cursor: pointer;
    height:2em;
    width: 2.5em;
    line-height: 3em;
    text-align: center;
}

.grid_item strong {
    color: rgb(170, 170, 170);
    font: bold 13px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    text-shadow: black 0px 0px 2px;
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    -webkit-transition: color 500ms;
}

.grid_item:hover strong {
    color: white;
}

.grid_item img {
    vertical-align: bottom;
    background: rgb(30, 30, 30);
    background: -webkit-gradient(linear, left top, left bottom, from(rgb(50, 50, 50)), to(rgb(10, 10, 10)));
    -webkit-box-reflect:below 1px -webkit-gradient(linear, left top, left 130%, from(transparent), color-stop(0.5, transparent), to(rgba(255, 255, 255, 0.5)));
}

Again, the CSS is pretty self explanatory, but one thing to note is that we set an initial font size of 75px which matches up with the initial value of the slider. We also set the label of the grid item, to have text-overflow: ellipsis, which means that if the width of the grid item becomes too small to fit the text in, three dots will appear at the end of the text to show that there is more. Again, this is a nice little touch, but it doesn’t work in Firefox.

We should now have a Grid View that looks something like this.

Step 7: The Mosaic View

My favorite view in Apple’s Web Gallery, is the Mosaic View. It displays a large view of the selected image on the left, and a grid of thumbnails on the right. When you resize your window, the whole interface resizes, so that the entire large view is always visible, and the thumbnail grid changes the number of columns that it displays. The only part that will ever need to scroll, is the grid on the right since the large image resizes to fit the window.

Apple also implements this view with JavaScript, setting an absolute position for each of the thumbnails, and reflowing the entire interface when you resize. We will recreate the interface with pure CSS, but first we need to generate our content with a bit of jQuery.

function mosaicView() {
    //remove everything from the content area that might have been there from other views,
    //add the grid_view class to the content view, and set up the title bar
    $("#content *").remove();
    $("#content").attr("class", "").addClass("mosaic_view");
    $("h1").addClass("view").html(title).show();
    $(".button").remove();

    //set up the footer view, and the content area
    $("#controls #slider").hide();
    $("#controls").show();
    $("#content").css({ bottom: "40px", top: "57px" });

    view = mosaicView; //set the current view

    //add the back button
    $('<div class="button">').html(gallery).click(function() {
        albumView(); //go back to the current view
    }).appendTo("body");

    //add the large view with title
    var detail = $('<div id="mosaic_detail">').click(function(i) {
        //largeView(this, current);
    });
    $("<img/>").attr("src", data[current].src).appendTo(detail);
    $("<strong/>").html(data[current].title).appendTo(detail);
    detail.appendTo("#content");

    //add the thubnail grid, with a click handler to animate the image change
    var grid = $('<div id="mosaic_grid">');
    $.each(data, function(i) {
        $('<div class="mosaic_item">')
            .css({
                backgroundPosition: "0px " + (-160 * i) + "px",
                backgroundImage: "url(" + sprite + ")"
            })
            .data("num", i)
            .click(function() {
                var num = $(this).data("num");
                current = num;
                $(".mosaic_item.selected").removeClass("selected");
                $(this).addClass("selected");

                $("#mosaic_detail").animate({ opacity: 0 }, "fast", function() {
                    $("#mosaic_detail img").attr("src", data[num].src);
                    $("#mosaic_detail strong").html(data[num].title);
                    $(this).animate({ opacity: 1 }, "fast");
                });
            }).appendTo(grid);
    });

    grid.appendTo("#content");

    //select the current item in the thumbnail grid view
    $(".mosaic_item:nth-child("+ (current + 1) +")").addClass("selected");
};

Once again, we do our usual reset at the beginning, and set the current view to mosaic view. The first thing that we create, is the large image on the left which again, if we had it, would trigger Apple’s large view. To hold this image, we create a div with the class of “mosaic_detail”, and add the currently selected image to it, along with the title label. The next thing we create, is the grid on the right. This happens once again in a $.each function. For each image in the album, we create a div with the “mosaic_item” class which will be added the parent div with the class of “mosaic_grid”. This item, will use the same CSS sprite that we used in our album view, in order to save on the number of images we need to download. We will set the background position based on the variable i, which is passed into the $.each function, and holds the index of the current image. Next, we use jQuery’s data function to store the index of the current image within the DOM element itself, and create a click handler to set the currently selected image. We use the data function to retrieve the index from the DOM element when it is clicked on, and set the global variable current to equal that index. Next, since we want to style the currently selected grid item in our CSS, we set the selected class to the item we clicked on, and remove the class from the previous selection if any. Finally, we animate the large view on the left to have an opacity of 0. This makes it invisible for a short period of time, while we change the image and title to the new selection. Then we animate back to visible. This creates a nice smooth effect when we change the selection.

Now that we have the content and interactivity set up, we can create the layout with our CSS. In order to make the layout flexible, we will use percentages and the max-width property. The diagram below shows the layout that we will create.

As you can see in the diagram, the large image on the left has a maximum height of 97%. This means that the height of the image will never exceed 97% of the height of the content area, but the current height is calculated so that the image remains proportional. The maximum width of the large image is set to 62%, and the grid view on the right has a left position of 64%, which creates a little space between the large image, and the grid. The grid has it’s width set to auto, which makes it extend to the edge of the browser window if necessary. It also has it’s overflow property set to auto, so that if there are more images in the grid than can fit in the given space, vertical scrollbars will appear. I’ve also set the minimum width of the grid, so that there will never be less than two columns. A minimum height on the large image is also set so that things don’t get too small, and the user knows to make their window larger. When a grid item becomes selected, the jQuery adds the selected class to the item, so that we can style it. The only thing we do with this, is to use the outline property to make a gray border around the image. We also make a thinner outline when the grid item is hovered over.

The CSS for creating this layout, as well as some styling to make it look nice, is below.

.mosaic_view {
    overflow: hidden !important;
    padding-top: 30px;
    min-width: 700px;
}

#mosaic_detail {
    width: 62%;
    min-width: 435px;
    height: 95%;
    cursor: pointer;
}

#mosaic_detail img {
    display: block;
    margin: auto;
    max-width: 100%;
    max-height: 97%;
    min-height: 212px;
    -webkit-box-reflect:below 1px -webkit-gradient(linear, left top, left 130%, from(transparent), color-stop(0.5, transparent), to(rgba(255, 255, 255, 0.4)));
}

#mosaic_detail strong {
    color: rgb(170, 170, 170);
    font: bold 13px 'Helvetica Neue', Helvetica, Arial, sans-serif;
    text-align: center;
    padding-bottom: 50px;
    padding-top: 5px;
    display: block;
    -webkit-transition: color 500ms;
}

#mosaic_detail:hover strong {
    color: white;
}

#mosaic_grid {
    position: absolute;
    left: 64%;
    top: 0px;
    bottom: 0px;
    width: auto;
    padding-top: 20px; /* 30 - 10 */
    padding-left: 2px;
    min-width: 250px;
    overflow: auto;
}

.mosaic_item {
    width: 95px;
    height: 95px;
    float: left;
    margin: 10px 10px 0 0;
    border: 1px solid black;
    cursor: pointer;
}

.mosaic_item:hover {
    outline: 1px solid rgb(170, 170, 170);
}

.mosaic_item.selected {
    outline: 2px solid rgb(170, 170, 170);
}

We should now have a flexible interface for our Mosaic View, that looks something like the screenshot below. You can see that the image on the left fits in it’s space, doesn’t overflow into other parts of the page, and remains proportional.

Conclusion and Disclaimers

Phew! We have finally come to the end! We should now have a fairly usable image gallery, and it is up to you to add features to it. If you connected it to a database, and wrote a nice back end to upload photos, you would be pretty close to having a complete product. Now on to the disclaimers:

  • If you try the demo in Internet Explorer (any version), it does not look how it was intended. I did not try to make it work in IE, but if you would like to do so, feel free (and post your fixes in the comments!).
  • I did not try to optimize the interface for load time (such as providing smaller images), so you may experience slowness the first time you load the images. This demo was purely to show how to create the interface itself.
  • I intentionally left out three of the views that Apple has for the sake of space: A larger view when you click on an image in the grid view or the large image in the mosaic view (which is commented out in the code), the slideshow view, and the carousel view.
  • All of the images used in the demo were taken from Apple’s demo MobileMe gallery.

If you have any questions or comments, please leave them here or send me a message on Twitter. I hope you enjoyed the tutorial!


Add your comment 1 Comments

Post your message and we'll contact you immediately.
Thank you for your desire to work with us.

Please, fill out the required fields!

Close
OK