Thumbnail Grid with Expanding Preview

A tutorial on how to create a thumbnail grid with an expanding image preview similar to the effect seen on Google Images.

Thumbnail Grid with Expanding Preview

From our sponsor: Try Mailchimp today.

If you have searched images on Google recently, you might have noticed the interesting expanding preview for a larger image when you click on a thumbnail. It’s a really nice effect and it is very practical, making a search much easier. Today we want to show you how to create a similar effect on a thumbnail grid. The idea is to open a preview when clicking on a thumbnail and to show a larger image and some other content like a title, a description and a link.

The interesting part is to calculate the correct preview height and to scroll the page to the right position. We’ll expand the preview in a way so that we can see the respective thumbnail row and cover the rest of the remaining page. Note that we don’t use very large images for the preview in the demo so you might see a lot of empty space on large monitors.

The demo features some amazing artwork by Jaime Martinez.

So let’s start!

The Markup

Initially, we need a thumbnail grid for which we will use an unordered list. Each list item will contain an anchor which will have several data attributes:

<ul id="og-grid" class="og-grid">
		<a href="" data-largesrc="images/1.jpg" data-title="Azuki bean" data-description="Swiss chard pumpkin bunya nuts maize plantain aubergine napa cabbage soko coriander sweet pepper water spinach winter purslane shallot tigernut lentil beetroot.">
			<img src="images/thumbs/1.jpg" alt="img01"/>
		<a href="" data-largesrc="images/2.jpg" data-title="Veggies sunt bona vobis" data-description="Komatsuna prairie turnip wattle seed artichoke mustard horseradish taro rutabaga ricebean carrot black-eyed pea turnip greens beetroot yarrow watercress kombu.">
			<img src="images/thumbs/2.jpg" alt="img02"/>
	<li><!-- ... --></li>
	<!-- ... -->

The href value will be used to construct the link in the preview description (this also comes in handy when JavaScript is disabled). The data-largesrc attribute contains the path to the larger image. data-title and data-description contain the title and description, respectively.

When we click on a thumbnail, we want a preview element to appear under the list item. For that we will need to insert an element into the grid. In fact, we will use the list item itself and add the preview element after the anchor:


	<a href="" data-largesrc="images/2.jpg" data-title="Veggies sunt bona vobis" data-description="Komatsuna prairie turnip wattle seed artichoke mustard horseradish taro rutabaga ricebean carrot black-eyed pea turnip greens beetroot yarrow watercress kombu.">
		<img src="images/thumbs/2.jpg" alt="img02"/>
	<div class="og-expander">
		<div class="og-expander-inner">
			<span class="og-close"></span>
			<div class="og-fullimg">
				<div class="og-loading"></div>
				<img src="images/2.jpg">
			<div class="og-details">
				<h3>Veggies sunt bona vobis</h3>
				<p>Komatsuna prairie turnip wattle seed artichoke mustard horseradish taro rutabaga ricebean carrot black-eyed pea turnip greens beetroot yarrow watercress kombu.</p>
				<a href="">Visit website</a>


Let’s style everything!


Note that the CSS will not contain any vendor prefixes, but you will find them in the files.

So, let’s start with the thumbnail grid. It will be full width and we’ll center the text. In this case this will mean that it will center the thumbnails because we’ll set them to display: inline-block:

.og-grid {
	list-style: none;
	padding: 20px 0;
	margin: 0 auto;
	text-align: center;
	width: 100%;

.og-grid li {
	display: inline-block;
	margin: 10px 5px 0 5px;
	vertical-align: top;
	height: 250px;

The links and images will be displayed as block elements and we’ll remove some default styling:

.og-grid li > a,
.og-grid li > a img {
	border: none;
	outline: none;
	display: block;
	position: relative;

When we click on an item, we will give a special class to the respective list item which will be called og-expanded. We’ll add a little arrow as pseudo-element to the anchor:

.og-grid li.og-expanded > a::after {
	top: auto;
	border: solid transparent;
	content: " ";
	height: 0;
	width: 0;
	position: absolute;
	pointer-events: none;
	border-bottom-color: #ddd;
	border-width: 15px;
	left: 50%;
	margin: -20px 0 0 -15px;

The preview itself will have the class og-expander and we’ll position that element absolutely. The initial height of the preview will be 0 and we’ll set the overflow to hidden:

.og-expander {
	position: absolute;
	background: #ddd;
	top: auto;
	left: 0;
	width: 100%;
	margin-top: 10px;
	text-align: left;
	height: 0;
	overflow: hidden;

.og-expander-inner {
	padding: 50px 30px;
	height: 100%;

The inner division will have some paddings and a height of 100%.

The cross for closing the preview will be created using pseudo-elements, i.e. two rotated lines:

.og-close {
	position: absolute;
	width: 40px;
	height: 40px;
	top: 20px;
	right: 20px;
	cursor: pointer;

.og-close::after {
	content: '';
	position: absolute;
	width: 100%;
	top: 50%;
	height: 1px;
	background: #888;
	transform: rotate(45deg);

.og-close::after {
	transform: rotate(-45deg);

.og-close:hover::after {
	background: #333;

The wrappers for the image and for the details will be 50% wide and we’ll make them float next to each other:

.og-details {
	width: 50%;
	float: left;
	height: 100%;
	overflow: hidden;
	position: relative;

The details wrapper will have some padding and we’ll center the image inside of the image wrapper by setting the text-align to center and the image itself to display: inline-block. The image will also have a max-height and max-width of 100% so that it adjusts its size to the surrounding container:

.og-details {
	padding: 0 40px 0 20px;

.og-fullimg {
	text-align: center;

.og-fullimg img {
	display: inline-block;
	max-height: 100%;
	max-width: 100%;

Let’s style the text elements and the link:

.og-details h3 {
	font-weight: 300;
	font-size: 52px;
	padding: 40px 0 10px;
	margin-bottom: 10px;

.og-details p {
	font-weight: 400;
	font-size: 16px;
	line-height: 22px;
	color: #999;

.og-details a {
	font-weight: 700;
	font-size: 16px;
	color: #333;
	text-transform: uppercase;
	letter-spacing: 2px;
	padding: 10px 20px;
	border: 3px solid #333;
	display: inline-block;
	margin: 30px 0 0;
	outline: none;

.og-details a::before {
	content: '2192';
	display: inline-block;
	margin-right: 10px;

.og-details a:hover {
	border-color: #999;
	color: #999;

The loading element will be in the same container as the image and we’ll not use any images but this CSS-only technique. We’ll create a little circle and set three box shadows: one for making the circle itself look a bit smoother and two for “copying” the element. Then we create an animation that will change the background color and the box shadow colors sequentially:

.og-loading {
	width: 20px;
	height: 20px;
	border-radius: 50%;
	background: #ddd;
	box-shadow: 0 0 1px #ccc, 15px 30px 1px #ccc, -15px 30px 1px #ccc;
	position: absolute;
	top: 50%;
	left: 50%;
	margin: -25px 0 0 -25px;
	animation: loader 0.5s infinite ease-in-out both;

@keyframes loader {
	0% { background: #ddd; }
	33% { background: #ccc; box-shadow: 0 0 1px #ccc, 15px 30px 1px #ccc, -15px 30px 1px #ddd; }
	66% { background: #ccc; box-shadow: 0 0 1px #ccc, 15px 30px 1px #ddd, -15px 30px 1px #ccc; }

Last, but not least, we’ll add two media queries for adjusting the text a bit and for hiding the full image once the screen gets so small that the preview image is not really useful anymore (we’ll also not load it in the JavaScript then).

@media screen and (max-width: 830px) {

	.og-expander h3 { font-size: 32px; }
	.og-expander p { font-size: 13px; }
	.og-expander a { font-size: 12px; }


@media screen and (max-width: 650px) {

	.og-fullimg { display: none; }
	.og-details { float: none; width: 100%; }

That’s all the style. Now, let’s take a look at the JavaScript.

The JavaScript

Let’s start by caching some elements and initializing some variables:

	// list of items
var $grid = $( '#og-grid' ),
	// the items
	$items = $grid.children( 'li' ),
	// current expanded item´s index
	current = -1,
	// position (top) of the expanded item
	// used to know if the preview will expand in a different row
	previewPos = -1,
	// extra amount of pixels to scroll the window
	scrollExtra = 0,
	// extra margin when expanded (between the preview element and the next item row)
	marginExpanded = 10,
	$window = $( window ), winsize,
	$body = $( 'html, body' ),
	// transitionend events
	transEndEventNames = {
		'WebkitTransition' : 'webkitTransitionEnd',
		'MozTransition' : 'transitionend',
		'OTransition' : 'oTransitionEnd',
		'msTransition' : 'MSTransitionEnd',
		'transition' : 'transitionend'
	transEndEventName = transEndEventNames[ Modernizr.prefixed( 'transition' ) ],
	// support for csstransitions
	support = Modernizr.csstransitions,
	// default settings
	settings = {
		minHeight : 500,
		speed : 350,
		easing : 'ease'

We will start by preloading all the images (thumbnails) in the grid. We then save the offset top and height for each item in the grid, get the current window’s size, and initialize some events:

	function init( config ) {
		// the settings..
		settings = $.extend( true, {}, settings, config );

		// preload all images
		$grid.imagesLoaded( function() {

			// save item´s size and offset
			saveItemInfo( true );
			// get window´s size
			// initialize some events

		} );


	// saves the item´s offset top and height (if saveheight is true)
	function saveItemInfo( saveheight ) {
		$items.each( function() {
			var $item = $( this );
			$ 'offsetTop', $item.offset().top );
			if( saveheight ) {
				$ 'height', $item.height() );
		} );

	function getWinSize() {
		winsize = { width : $window.width(), height : $window.height() };

We will bind the click event for each item (anchor) and for the close button (when the item is opened). When we click on an item, the preview with the large image source and the details will be revealed or hidden if already shown. If we click the close button (cross) on the preview then this preview will be closed too.
We are also binding the resize event for the window, where some values are reset and the preview gets closed (if opened).

	function initEvents() {
		// when clicking an item, show the preview with the item´s info and large image;
		// close the item if already expanded.
		// also close if clicking on the item´s cross
		$items.on( 'click', 'span.og-close', function() {
			return false;
		} ).children( 'a' ).on( 'click', function(e) {

			var $item = $( this ).parent();
			// check if item already opened
			current === $item.index() ? hidePreview() : showPreview( $item );
			return false;

		} );

		// on window resize get the window´s size again
		// reset some values..
		$window.on( 'debouncedresize', function() {
			scrollExtra = 0;
			previewPos = -1;
			// save item´s offset
			var preview = $.data( this, 'preview' );
			if( typeof preview != 'undefined' ) {

		} );


With the showPreview function we will basically initialize the Preview object, which in turn will expand and reveal the details and the large version of the image. If a Preview instance is already initialized then we will only update the preview with the new details (if the clicked item is in the same row as the current expanded item) or hide it and initialize / open a new one (if not in the same row).
In order to check if the items are in the same row as the current preview, we use the offset top value of the items.

	function showPreview( $item ) {

		var preview = $.data( this, 'preview' ),
			// item´s offset top
			position = $ 'offsetTop' );

		scrollExtra = 0;

		// if a preview exists and previewPos is different (different row) from item´s top, then close it
		if( typeof preview != 'undefined' ) {

			// not in the same row
			if( previewPos !== position ) {
				// if position > previewPos then we need to take the current preview´s height in consideration when scrolling the window
				if( position > previewPos ) {
					scrollExtra = preview.height;
			// same row
			else {
				preview.update( $item );
				return false;

		// update previewPos
		previewPos = position;
		// initialize new preview for the clicked item
		preview = $.data( this, 'preview', new Preview( $item ) );
		// expand preview overlay;


The Preview object will have a reference to the currently displayed item (Preview.$item), and the index of the expanded item (Preview.expandedIdx). Note that the expanded item is not necessarily the displayed item. For instance if we click on a second item that is on the same row as the one clicked before then the Preview will be “reused” and the Preview.expandedIdx will not be the index of the Preview.$item. We need to keep the reference to the expanded item so that when the Preview is closed we know which item to “collapse”.

	// the preview obj / overlay
	function Preview( $item ) {
		this.$item = $item;
		this.expandedIdx = this.$item.index();

As the Preview object is initialized we create the necessary structure where the item´s details will be rendered and we append it to the item:

	create : function() {
		// create Preview structure:
		this.$title = $( '<h3></h3>' );
		this.$description = $( '<p></p>' );
		this.$href = $( '<a href="#">Visit website</a>' );
		this.$details = $( '<div class="og-details"></div>' ).append( this.$title, this.$description, this.$href );
		this.$loading = $( '<div class="og-loading"></div>' );
		this.$fullimage = $( '<div class="og-fullimg"></div>' ).append( this.$loading );
		this.$closePreview = $( '<span class="og-close"></span>' );
		this.$previewInner = $( '<div class="og-expander-inner"></div>' ).append( this.$closePreview, this.$fullimage, this.$details );
		this.$previewEl = $( '<div class="og-expander"></div>' ).append( this.$previewInner );
		// append preview element to the item
		this.$item.append( this.getEl() );
		// set the transitions for the preview and the item
		if( support ) {

Then we fill the previous structure with the item´s details (stored in data attributes and the href).
The update function will also be used to just update the content of an existing preview.

	update : function( $item ) {

		// update with new item´s details 
		if( $item ) {
			this.$item = $item;
		// if already expanded, remove class "og-expanded" from current item and add it to new item
		if( current !== -1 ) {
			var $currentItem = $items.eq( current );
			$currentItem.removeClass( 'og-expanded' );
			this.$item.addClass( 'og-expanded' );
			// position the preview correctly

		// update current value
		current = this.$item.index();

		// update preview´s content
		var $itemEl = this.$item.children( 'a' ),
			eldata = {
				href : $itemEl.attr( 'href' ),
				largesrc : $ 'largesrc' ),
				title : $ 'title' ),
				description : $ 'description' )

		this.$title.html( eldata.title );
		this.$description.html( eldata.description );
		this.$href.attr( 'href', eldata.href );

		var self = this;
		// remove the current image in the preview
		if( typeof self.$largeImg != 'undefined' ) {

		// preload large image and add it to the preview
		// for smaller screens we don´t display the large image (the last media query will hide the wrapper of the image)
		if( self.$ ':visible' ) ) {
			$( '<img/>' ).load( function() {
				self.$largeImg = $( this ).fadeIn( 350 );
				self.$fullimage.append( self.$largeImg );
			} ).attr( 'src', eldata.largesrc );	


To reveal the preview we need to set the height of the Preview element and also of the item (to push down the items below). The height of the preview will be the window´s height minus the height of the grid item. To avoid cases where that height could be too small we add the option “minHeight” where we can specify the minimum height needed for the preview element.
As the preview opens we will want to scroll the window so that the preview is completely visible (and if possible, also the item).

	open : function() {

		setTimeout( $.proxy( function() {	
			// set the height for the preview and the item
			// scroll to position the preview in the right place
		}, this ), 25 );


	setHeights : function() {

		var self = this,
			onEndFn = function() {
				if( support ) {
					self.$ transEndEventName );
				self.$item.addClass( 'og-expanded' );

		this.$previewEl.css( 'height', this.height );
		this.$item.css( 'height', this.itemHeight ).on( transEndEventName, onEndFn );

		if( !support ) {;


	calcHeight : function() {

		var heightPreview = winsize.height - this.$ 'height' ) - marginExpanded,
			itemHeight = winsize.height;

		if( heightPreview < settings.minHeight ) {
			heightPreview = settings.minHeight;
			itemHeight = settings.minHeight + this.$ 'height' ) + marginExpanded;

		this.height = heightPreview;
		this.itemHeight = itemHeight;


	positionPreview : function() {

		// scroll page
		// case 1 : preview height + item height fits in window´s height
		// case 2 : preview height + item height does not fit in window´s height and preview height is smaller than window´s height
		// case 3 : preview height + item height does not fit in window´s height and preview height is bigger than window´s height
		var position = this.$ 'offsetTop' ),
			previewOffsetT = this.$previewEl.offset().top - scrollExtra,
			scrollVal = this.height + this.$ 'height' ) + marginExpanded <= winsize.height ? position : this.height < winsize.height ? previewOffsetT - ( winsize.height - this.height ) : previewOffsetT;
		$body.animate( { scrollTop : scrollVal }, settings.speed );


When closing the preview we reset the heights of the preview element and the expanded item. Once this is done, the preview element / structure gets removed from the DOM.

	close : function() {

		var self = this,
			onEndFn = function() {
				if( support ) {
					$( this ).off( transEndEventName );
				self.$item.removeClass( 'og-expanded' );

		setTimeout( $.proxy( function() {

			if( typeof this.$largeImg !== 'undefined' ) {
				this.$largeImg.fadeOut( 'fast' );
			this.$previewEl.css( 'height', 0 );
			// the current expanded item (might be different from this.$item)
			var $expandedItem = $items.eq( this.expandedIdx );
			$expandedItem.css( 'height', $ 'height' ) ).on( transEndEventName, onEndFn );

			if( !support ) {;

		}, this ), 25 );
		return false;


And that's it! We hope you enjoyed this tutorial and find it inspiring!

Tagged with:

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

Feedback 477

Comments are closed.
  1. I always enjoy your tutorials on CoDrop. Most are both useful as well as incredibly stylish. When I first saw this style of animation on Google Images, I was really impressed. I hope I can find a way to use it in the future.

  2. Awesome! I think keyboard navigation is something that might add more awesomeness 🙂

  3. here’s problem with mobile devices – when we expand item and then close it, there a lot of space below icon

  4. No seriously, do you read my mind? I was desperately looking for this one. Tnx a zillion

  5. I’m not sure if you’re aware, but in IE8 there’s a margin left over after you click the close (minus) button.

  6. One downside. If you resize the window, e. g. you want to view a picture bigger, the big picture is closed. But the rest is nice. 🙂

  7. I was wondering when you guys were going to figure out how Google did this. Thanks so much!

  8. simply awesome! Have no words! out of the world. Thanks for sharing and keep doing this kinda wonderful work.

  9. You guys are the freaking bomb-diggity!! Thanks so much for ANOTHER GREAT tutorial!!!

  10. Great tut I’ve been working on this myself recently and have a version of it working using isotope). This could have saved me a fair bit of time 😉

  11. Hello … When you use this code on a mobile, there is problem with the slideDown. At the click on an image, sometimes the details close up instantly. Please can you correct it :)))) PS: Awsome tutorial !!!

  12. Very nice but there are issues with Internet Explorer 9 and below: the slide-up on close hides the panel but doesn’t remove the space it occupied, messing up the layout.

  13. Again, proving to us all that tympanus/co-drops is one of the best learning resources on the web today, amongst my favorites anyway! You need to add a tweet button to the tutorials so we can share/bookmark this fantastic content more easily. Seen this shared via Smashing Magazine!

  14. Finalmente algum artigo! Estava muito ansioso pra saber quando sairia o próximo, que ficou excelente. Parabéns

  15. Litterally can’t thank you enough for this. I learned so much! If anyone figures out a way to fix the mobile auto recollapsing, please email me! Super important!

  16. you should add closing the expanded view by hitting Escape button. That’s the way google does, too

  17. Works very nice, except for mobile devices, on my iPhone it opens and closes immediately!

    To make items without website:
    – Put href =”#”
    – Change the grid.js, around line 365, find:
    this.$title.html( eldata.title ); this.$description.html( eldata.description ); this.$href.attr( 'href', eldata.href );
    – Replace with:
    this.$title.html( eldata.title ); this.$description.html( eldata.description ); if(eldata.href == '#') { this.$href.hide(); } else { this.$; this.$href.attr( 'href', eldata.href ); }

  18. I really love this amazing webside! Thanks so much Mary Lou! I´m creating a webside for students using some plugins from this webside. THANKS THANKS THANKS FROM SPAIN! 😉

  19. Why do you guys choose to ignore Internet Explorer 9 and below? It still holds a significant market share and makes all the cool stuff you guys built unusable in a standard deployment.

    • I think it goes the other way…
      Internet Explorer ignore what developers require nowadays, and make everything possible to complicate us.

      People should stop using IE.

  20. Hello Mary Lou, Goodbye heart, Sweet Mary Lou, I’m so in love with you! 🙂
    Great article, i was looking to build it myself, but you beat me to it. Also, it would be nice to have a Themeforest style Screenshot preview, does someone know for a good tutorial on that?

  21. Hi, thanks a ton Mary Lou…always great to visit this site. Nice tutorial n share..!

  22. Great tutorial…I’d like to use it in a website but before I need to find a fix for IE8-IE9 white space remaining after closing a detail. Does anyone find a solution?
    Thanks in advance,

  23. Nice tutorial. On the other hand its pretty useless if it does not have IE support, since that browser is used by the majority of people.

    So, anyone have a IE fix for this?

    • Nice!

      I notice on yours when you resize the window, the enlarged image stays up, unlike on this one. Any pointers on how to change this one to do that too?

    • I love Codrop’s tutorials, but I have to say that your demo fits my purposes better as it allows for easier customization of the expanded window. Great job!