Beautiful Photo Stack Gallery with jQuery and CSS3

In this tutorial we are going to create a nice and fresh image gallery. The idea is to show the albums as a slider, and when an album is chosen, […]

In this tutorial we are going to create a nice and fresh image gallery. The idea is to show the albums as a slider, and when an album is chosen, we show the images of that album as a beautiful photo stack. In the photo stack view, we can browse through the images by putting the top most image behind all the stack with a slick animation.

We will use jQuery and CSS3 properties for the rotated image effect. We will also use the webkit-box-reflect property in order to mirror the boxes in the album view – check out the demo in Google Chrome or Apple Safari to see this wonderful effect.

We will also use a bit of PHP for getting the images from each album.

So, let’s start!

The Markup

In our HTML structure we will have several elements. The main ones are for the album columns with thumbnail and description, and the preview with the photo stack.
The structure for the album view is going to look as follows:

<div id="ps_slider" class="ps_slider">

	<a class="prev disabled"></a>
	<a class="next disabled"></a>
	
	<div id="ps_albums">
	
		<div class="ps_album" style="opacity:0;">
			<img src="albums/album1/thumb/thumb.jpg" alt=""/>
			<div class="ps_desc">
				<h2>Album title</h2>
				<span>Description text</span>
			</div>
		</div>
		
		<div class="ps_album" style="opacity:0;">
			...
		</div>
		
		...
	</div>	
	
</div>

The opacity of the album div is going to be 0 in the beginning, we will then use JavaScript to fade in the columns.
The information that we need to provide in the HTML is the location of each album and its thumbnail images. With our little PHP script we will then get all the images from the respective album.

The structure for the dark overlay and the preview with the photo stack is going to look like this:

<div id="ps_overlay" class="ps_overlay" style="display:none;"></div>

<a id="ps_close" class="ps_close" style="display:none;"></a>

<div id="ps_container" class="ps_container" style="display:none;">
	<a id="ps_next_photo" class="ps_next_photo" style="display:none;"></a>
</div>

We will dynamically insert the pictures of each album into the ps_container div.
Now, let’s take a look at the PHP.

The PHP

Our PHP file will be called from our JavaScript and we will get the information with AJAX call. The PHP looks as follows:

$location 	= 'albums';
$album_name	= $_GET['album_name'];
$files 		= glob($location . '/' . $album_name . '/*.{jpg,gif,png}', GLOB_BRACE);
$encoded 	= json_encode($files);
echo $encoded;
unset($encoded);

The CSS

The style for the gallery is going to contain some CSS3 properties and also a Webkit specific property for a mirror effect.
Let’s start by resetting some paddings and margins and defining the general style for the body:

body, ul, li, h1, h2, h3{
	margin:0;
	padding:0;
}
body{
	background:#121212;
	font-family:Arial, Helvetica, sans-serif;
	font-size:11px;
	color:#fff;
	overflow-x:hidden;
}

We will continue with the style of the preview. The overlay is going to have the following style:

.ps_overlay{
    z-index:90;
    background:#111;
    width:100%;
    height:100%;
    position:fixed;
    top:0px;
    left:0px;
    opacity:0.5;
}

The container for the photo stack will have a defined height and width. We will position it absolutely and center it with the “50%/negative margins” method:

.ps_container{
	width:480px;
	height:350px;
	position:absolute;
	top:50%;
	margin-top:-175px;
	left:50%;
	margin-left:-240px;
	z-index:100;
}

The image will have a thick white border and a nice box shadow. We will center the image relatively to its container but since we will only know the width and height of the images once we dynamically add them, we will set the margins in our JavaScript function:

.ps_container img{
	border:10px solid #fff;
	position:absolute;
	top:50%;
	left:50%;
	-moz-box-shadow:1px 1px 10px #000;
	-webkit-box-shadow:1px 1px 10px #000;
	box-shadow:1px 1px 10px #000;
}

The close button for the preview is going to be fixed at the top right of the window:

a.ps_close{
	background:#000 url(../images/close.png) no-repeat center center;
	cursor:pointer;
	width:56px;
	height:56px;
	position:fixed;
	right:10px;
	top:10px;
	z-index:1000;
	-moz-border-radius:10px;
	-webkit-border-radius:10px;
	border-radius:10px;	
	opacity:0.6;
}

We will show a “next photo” element on top of the current image when the user hovers over it:

a.ps_next_photo{
	position:absolute;
	top:50%;
	left:50%;
	width:56px;
	height:56px;
	margin:-28px 0 0 -28px;
	z-index:200;
	cursor:pointer;
	background:#000 url(../images/next_photo.png) no-repeat 50% 50%;
	opacity:0.6;
    -moz-border-radius:10px;
    -webkit-border-radius:10px;
    border-radius:10px;
}

The hover classes for the last two elements:

a.ps_next_photo:hover,
a.ps_close:hover{
	opacity:0.8;	
}

Now we are going to define the style for the album view and its columns. The slider container will be positioned relatively. With the auto values for the left and right margins we center the container horizontally on the page:

.ps_slider{
	width:845px;
	height:300px;
	position:relative;
	margin:110px auto 0px auto;
}

The navigation elements are going to have the following style:

.ps_slider a.next,
.ps_slider a.prev{
	position:absolute;
	background-color:#000;
    background-position:center center;
    background-repeat:no-repeat;
    border:1px solid #232323;
    width:20px;
    height:20px;
    top:50%;
    margin-top:-10px;
    opacity:0.6;
    -moz-border-radius:5px;
    -webkit-border-radius:5px;
    border-radius:5px;
    cursor:pointer;
    outline:none;
}
.ps_slider a.prev{
    left:-30px;
    background-image:url(../images/prev.png);
}
.ps_slider a.next{
    right:-30px;
    background-image:url(../images/next.png);
}
.ps_slider a.prev:hover,
.ps_slider a.next:hover{
    border:1px solid #333;
    opacity:0.9;
}

We also define a class for the navigation elements when they are disabled:

.ps_slider a.disabled,
.ps_slider a.disabled:hover{
    opacity:0.4;
    border:1px solid #111;
    cursor:default;
}

Each album column is going to have the following style:

.ps_slider .ps_album{
	width:140px;
	height:310px;
	padding:10px;
	background-color:#333;
	border:1px solid #444;
	position:absolute;
	top:0px;
	text-align:center;
	cursor:pointer;
	-moz-box-shadow:1px 1px 4px #000;
	-webkit-box-shadow:1px 1px 4px #000;
	box-shadow:1px 1px 4px #000;
	-webkit-box-reflect:
		below 5px 
		-webkit-gradient(
			linear, 
			left top, 
			left bottom, 
			from(transparent), 
			color-stop(0.6, transparent), 
			to(rgb(18, 18, 18))
		);
}

The last property is the Webkit box reflex that mirrors the columns. We say that it should mirror the element in a gradient like way.

We add a hover class for the whole column:

.ps_slider .ps_album:hover{
		background-color:#383838;
}

The thumbnail image and the content of the column are going to have the following styles:

.ps_slider .ps_album img{
	height:90px;
	border:1px solid #444;
	-moz-box-shadow:1px 1px 4px #000;
	-webkit-box-shadow:1px 1px 4px #000;
	box-shadow:1px 1px 4px #000;
}
.ps_slider .ps_album .ps_desc{
	display:block;
	color:#666;
	background:#111 url(../images/overlay.png) no-repeat bottom right;
	height:200px;
	margin-top:10px;
	text-align:left;
	line-height:20px;
	overflow:hidden;
	text-overflow:ellipsis;
	border:1px solid #393939;
	-moz-box-shadow:0px 0px 2px #000 inset;
	-webkit-box-shadow:0px 0px 2px #000 inset;
	box-shadow:0px 0px 2px #000 inset;
}
.ps_slider .ps_album:hover .ps_desc{
	background-image:none;
}
.ps_slider .ps_album .ps_desc span{
	display:block;
	margin:0px 10px 10px 10px;
	border-top:1px solid #333;
	padding-top:5px;
}
.ps_slider .ps_album .ps_desc h2{
	margin:10px 10px 0px 10px;
	text-align:left;
	padding-bottom:5px;
	font-weight:normal;
	color:#ddd;
	text-shadow:0px 0px 1px #fff;
	border-bottom:1px solid #000;
}

The loading element will show when we click on an album and all the images get loaded in the preview:

.ps_slider .loading{
	background:#121212 url(../images/loading.gif) no-repeat 50% 50%;
	position:absolute;
	top:0px;
	left:0px;
	width:100%;
	height:100%;
	opacity:0.7;
}

For the opacity to work in IE you need to add this filter (with the right value):

filter:progid:DXImageTransform.Microsoft.Alpha(opacity=60);

I did not want to uglify the code.

Let’s add the magic!

The JavaScript

First, we need to define some variables that help us control the album navigation and keep some elements. The variable positions saves the left value for each album column. This depends on the width of the column.

/**
* navR,navL are flags for controlling the albums navigation
* first gives us the position of the album on the left
* positions are the left positions for each of the 5 albums displayed at a time
*/
var navR,navL	= false;
var first		= 1;
var positions 	= {
	'0'		: 0,
	'1' 	: 170,
	'2' 	: 340,
	'3' 	: 510,
	'4' 	: 680
}
var $ps_albums 		= $('#ps_albums');
/**
* number of albums available
*/
var elems			= $ps_albums.children().length;
var $ps_slider		= $('#ps_slider');

We are going to give an initial positioning to the album columns:


/**
* position all the albums on the right side of the window
*/
var hiddenRight 	= $(window).width() - $ps_albums.offset().left;
$ps_albums.children('div').css('left',hiddenRight + 'px');

/**
* move the first 5 albums to the viewport
*/
$ps_albums.children('div:lt(5)').each(
	function(i){
		var $elem = $(this);
		$elem.animate({'left': positions[i] + 'px','opacity':1},800,function(){
			if(elems > 5)
				enableNavRight();
		});
	}
);

For sliding through the albums, we will define what happens when we click on the next or previous button and create two functions for moving left or right:

/**
* next album
*/
$ps_slider.find('.next').bind('click',function(){
	if(!$ps_albums.children('div:nth-child('+parseInt(first+5)+')').length || !navR) return;
	disableNavRight();
	disableNavLeft();
	moveRight();
});

/**
* we move the first album (the one on the left) to the left side of the window
* the next 4 albums slide one position, and finally the next one in the list
* slides in, to fill the space of the first one
*/
function moveRight () {
	var hiddenLeft 	= $ps_albums.offset().left + 163;
	
	var cnt = 0;
	$ps_albums.children('div:nth-child('+first+')').animate({'left': - hiddenLeft + 'px','opacity':0},500,function(){
			var $this = $(this);
			$ps_albums.children('div').slice(first,parseInt(first+4)).each(
				function(i){
					var $elem = $(this);
					$elem.animate({'left': positions[i] + 'px'},800,function(){
						++cnt;
						if(cnt == 4){
							$ps_albums.children('div:nth-child('+parseInt(first+5)+')').animate({'left': positions[cnt] + 'px','opacity':1},500,function(){
								//$this.hide();
								++first;
								if(parseInt(first + 4) < elems)
									enableNavRight();
								enableNavLeft();
							});
						}		
					});
				}
			);		
	});
}

/**
* previous album
*/
$ps_slider.find('.prev').bind('click',function(){
	if(first==1  || !navL) return;
	disableNavRight();
	disableNavLeft();
	moveLeft();
});

/**
* we move the last album (the one on the right) to the right side of the window
* the previous 4 albums slide one position, and finally the previous one in the list
* slides in, to fill the space of the last one
*/
function moveLeft () {
	var hiddenRight 	= $(window).width() - $ps_albums.offset().left;

	var cnt = 0;
	var last= first+4;
	$ps_albums.children('div:nth-child('+last+')').animate({'left': hiddenRight + 'px','opacity':0},500,function(){
			var $this = $(this);
			$ps_albums.children('div').slice(parseInt(last-5),parseInt(last-1)).each(
				function(i){
					var $elem = $(this);
					$elem.animate({'left': positions[i+1] + 'px'},800,function(){
						++cnt;
						if(cnt == 4){
							$ps_albums.children('div:nth-child('+parseInt(last-5)+')').animate({'left': positions[0] + 'px','opacity':1},500,function(){
								//$this.hide();
								--first;
								enableNavRight();
								if(first > 1)
									enableNavLeft();
							});
						}										
					});
				}
			);
	});
}

The next helper functions deal with disabling and enabling the the navigation items:

/**
* disable or enable albums navigation
*/
function disableNavRight () {
	navR = false;
	$ps_slider.find('.next').addClass('disabled');
}
function disableNavLeft () {
	navL = false;
	$ps_slider.find('.prev').addClass('disabled');
}
function enableNavRight () {
	navR = true;
	$ps_slider.find('.next').removeClass('disabled');
}
function enableNavLeft () {
	navL = true;
	$ps_slider.find('.prev').removeClass('disabled');
}		

In the next steps we will define what happens when we open an album. We first make the loading element appear and show the preview after we loaded all the images. The information of the source comes from an AJAX call to our PHP class. For the rotation effect, we use the rotate CSS3 property which we restrict to a certain range of degrees, so that we don’t rotate an image crazily:

var $ps_container 	= $('#ps_container');
var $ps_overlay 	= $('#ps_overlay');
var $ps_close		= $('#ps_close');
/**
* when we click on an album,
* we load with AJAX the list of pictures for that album.
* we randomly rotate them except the last one, which is
* the one the User sees first. We also resize and center each image.
*/
$ps_albums.children('div').bind('click',function(){
	var $elem = $(this);
	var album_name 	= 'album' + parseInt($elem.index() + 1);
	var $loading 	= $('<div />',{className:'loading'});
	$elem.append($loading);
	$ps_container.find('img').remove();
	$.get('photostack.php', {album_name:album_name} , function(data) {
		var items_count	= data.length;
		for(var i = 0; i < items_count; ++i){
			var item_source = data[i];
			var cnt 		= 0;
			$('<img />').load(function(){
				var $image = $(this);
				++cnt;
				resizeCenterImage($image);
				$ps_container.append($image);
				var r		= Math.floor(Math.random()*41)-20;
				if(cnt < items_count){
					$image.css({
						'-moz-transform'	:'rotate('+r+'deg)',
						'-webkit-transform'	:'rotate('+r+'deg)',
						'transform'			:'rotate('+r+'deg)'
					});
				}
				if(cnt == items_count){
					$loading.remove();
					$ps_container.show();
					$ps_close.show();
					$ps_overlay.show();
				}
			}).attr('src',item_source);
		}
	},'json');
});

/**
* when hovering each one of the images, 
* we show the button to navigate through them
*/
$ps_container.live('mouseenter',function(){
	$('#ps_next_photo').show();
}).live('mouseleave',function(){
	$('#ps_next_photo').hide();
});

The nice animation of putting the current image at the back of our stack is defined in the following:

/**
* navigate through the images: 
* the last one (the visible one) becomes the first one.
* we also rotate 0 degrees the new visible picture 
*/
$('#ps_next_photo').bind('click',function(){
	var $current 	= $ps_container.find('img:last');
	var r			= Math.floor(Math.random()*41)-20;
	
	var currentPositions = {
		marginLeft	: $current.css('margin-left'),
		marginTop	: $current.css('margin-top')
	}
	var $new_current = $current.prev();
	
	$current.animate({
		'marginLeft':'250px',
		'marginTop':'-385px'
	},250,function(){
		$(this).insertBefore($ps_container.find('img:first'))
			   .css({
					'-moz-transform'	:'rotate('+r+'deg)',
					'-webkit-transform'	:'rotate('+r+'deg)',
					'transform'			:'rotate('+r+'deg)'
				})
			   .animate({
					'marginLeft':currentPositions.marginLeft,
					'marginTop'	:currentPositions.marginTop
					},250,function(){
						$new_current.css({
							'-moz-transform'	:'rotate(0deg)',
							'-webkit-transform'	:'rotate(0deg)',
							'transform'			:'rotate(0deg)'
						});
			   });
	});
});

We animate the top and left margins with certain values that create the “putting back” effect. Since our structure always shows the last image on the top, we will also need to insert the moving image at the beginning, so that it appear as last one in the stack.

Clicking on the close element brings us back to the album view:

/**
* close the images view, and go back to albums
*/
$('#ps_close').bind('click',function(){
	$ps_container.hide();
	$ps_close.hide();
	$ps_overlay.fadeOut(400);
});

Our famous resize function resizes the image according to the given container width and height.
In the end of the function we apply the sizes and we also apply the negative margins – remember, that’s the way we can center an absolute positioned element; we defined top and left in the CSS to be 50%, so now we need to pull it to the right place with these margin values.

/**
* resize and center the images
*/
function resizeCenterImage($image){
	var theImage 	= new Image();
	theImage.src 	= $image.attr("src");
	var imgwidth 	= theImage.width;
	var imgheight 	= theImage.height;
	
	var containerwidth  = 460;
	var containerheight = 330;
	
	if(imgwidth	> containerwidth){
		var newwidth = containerwidth;
		var ratio = imgwidth / containerwidth;
		var newheight = imgheight / ratio;
		if(newheight > containerheight){
			var newnewheight = containerheight;
			var newratio = newheight/containerheight;
			var newnewwidth =newwidth/newratio;
			theImage.width = newnewwidth;
			theImage.height= newnewheight;
		}
		else{
			theImage.width = newwidth;
			theImage.height= newheight;
		}
	}
	else if(imgheight > containerheight){
		var newheight = containerheight;
		var ratio = imgheight / containerheight;
		var newwidth = imgwidth / ratio;
		if(newwidth > containerwidth){
			var newnewwidth = containerwidth;
			var newratio = newwidth/containerwidth;
			var newnewheight =newheight/newratio;
			theImage.height = newnewheight;
			theImage.width= newnewwidth;
		}
		else{
			theImage.width = newwidth;
			theImage.height= newheight;
		}
	}
	$image.css({
		'width'			:theImage.width,
		'height'		:theImage.height,
		'margin-top'	:-(theImage.height/2)-10+'px',
		'margin-left'	:-(theImage.width/2)-10+'px'	
	});
}
});

And that’s it! We hope you enjoyed this tutorial and find it useful!

P.S. Because of some Webkit properties, this demo is a really nice experience if you use a Webkit browser like Chrome or Safari. It will also be fully functional in all latest browsers like Firefox and IE (without those beautiful properties, though).

Message from TestkingWe offer professional testking RH302 training with testking 646-671 video tutorials and testking 70-643 guide to help the designers learn how to create beautiful website designs using Jquery and css.

Tagged with:

Manoela Ilic

Manoela is the main tinkerer at Codrops. With a background in coding and passion for all things design, she creates web experiments and keeps frontend professionals informed about the latest trends.

Stay up to date with the latest web design and development news and relevant updates from Codrops.

Feedback 190

Comments are closed.
  1. Really cool gallery. It would be better if you place “Demo and Download” buttons at top.

  2. Excellent! Only one tip: you might want to put in an easier method to ‘escape’ the stack/return to the overview by clicking on the translucent ligthbox area or by keystroke (ESC), like Fancybox does.

  3. +1 for making it easier to escape! In a wide browser its difficult to realise that there is an “x” in the corner! First time I had to press back to escape.

    It is beautiful though 🙂 good work

  4. Awesome, although it needs to close on Esc keypress and when clicking outside of the ‘stack’ but I can implement that when I use it.

    Otherwise, really nice. Will take some time cropping all my images to the same size mind, but it’s worth it.

    PS, Just realised someone made the same comment as me and me as them. D’oh

  5. Great tutorial… you guys at codrops have a perpetual flow of ideas… great work!

    • We appreciate you asking and we always love to see where our ideas get applied to, and although we do not really favor any commercialization, we are Anti-copyright. So our license proudly is: DWTFYWWI 😀
      Power to the freedom of knowledge!
      Cheers, ML

  6. I think this is very well organized and thought of but the transitions were a bit slow on my firefox (latest 3.6) but that may just be my browser since I have 18 tabs.
    Well Done!

  7. Hi !!
    this script is really nice.
    But juste one question :
    How we make to show only 3 albums at the beginning of the animation ?

    (sorry for my bad english)

  8. When I click on a category, the loader appears but no photo comes in front of the screen as in the demo of the website …

    Someone could help me please ?

  9. @Stephane

    Seems to me like it’s a path issue. The default path for photos is the /albums folder. If you are using something else than make the modifications accordingly in the photostack.php file.

    @weby
    Look in the html code. Pretty self-explanatory.

  10. –Bug IE–
    for(var i = 0; i < items_count; ++i){
    var item_source = data[i];
    var cnt = 0;
    $('’).load(function(){
    var $image = $(this);
    ++cnt;

    –Bug IE Resolved–
    var cnt =0;
    for(var i = 0; i < items_count; ++i){
    var item_source = data[i]; $('’).load(function(){
    $(‘.ps_overlay’).css({
    ‘opacity’:0.5,
    ‘background’ :’#111′,
    });

    var $image = $(this);
    ++cnt;

  11. @Nik You need to put it into a server environment because you need to be able to run the PHP code. You can use something like XAMPP to run it on your machine. Hope it helps, cheers, ML

  12. I have tried it but it seems that it doesn’t work very well in Firefox or in IE, as Stephane says when you click on an album the loader appears but no photo comes out.

    I’m sure it’s not a path issue because i’ve try it in Chrome and works very well.

    Does someone knows why?

    In Chrome looks amazing, great work!!!

  13. not up to the previous well tested crossbrowser standard of earlier posts.

    many bugs.
    otherwise, somewhat interesting, would not currently recommend

    • Dear Steve, could you please specify what bugs you mean besides the double click one mentioned earlier and what browsers you are testing with? We tested in Google Chrome 5.0.375.99, Mozilla Firefox 3.5.30729, Internet Explorer 8.0.6001, Opera 9.80 and Safari 5.0 (7533.16) and it works fine. It’s kind of out of place to talk about cross browser standards when including CSS3, don’t you think? Cheers, ML

  14. How do I get the albums to display? Is there a more detailed tutorial for beginners?

  15. Very cool work…we’re having fun with image collecting and sharing too with Scrwall! You can add infinite images to your collages and view by dragging around the screen. Keep up the great work!

  16. Hi, i’ve uploaded the files to my server and everything works and looks great, but when I tried to integrate it with wordpress the album just won’t show up, i believe it has to do with the path of the images or maybe wordpress can’t find the php file used for the path. Can anyone help?

  17. Hi,
    like some others people, i have installed all the files and when I click on one album I just have the loeader and no images appears.

    It don’t work on any browsers (Chrome, Firefox, …) and I try to change some path in photostack.php but without any effects. Someone can help me please?

    I must change something if my albums are on the path http://www.mysite.com/albums/album1?

    Thanks in advance 🙂

  18. Hi,

    Like Alain, I am also having difficulties in loading the images. I see the loader and only the loader. Path was not changed either.

    Awaiting for help to get this beautiful piece of work up on our website!

  19. Can we have the html version only to run on the static sites? What if we dont use the php here? any possible solution

  20. Also had problem with no images,
    But did get it fixed.
    PHP function json_encode() only works in PHP 5.2 or higher.
    But there is a work around where you add one line to the top of photostack.php
    require ‘jsonwrapper.php’;
    and download and save jsonwrapper.php from http://www.boutell.com/scripts/jsonwrapper.html

  21. I change the photos, but after I changed my size or photo above or fraud
    how to do change photo size and name
    thanks

  22. I created a system so that access a MySQL database and load the images dynamically.

    If anyone wants, just tell me.

    [] ‘S