Product Grid Layout

A responsive product grid layout with some UI details for inspiration.

ProductGridLayout

A responsive product grid layout that comes with some UI details for inspiration. The product will rotate showing the backside of the item when the rotate button is clicked. Some examples of how tooltips can appear on hover or click are also included. Media queries can be used to resize the items in the grid or change the number of items in a row. Flexbox is used when supported.

The HTML

<div id="cbp-pgcontainer" class="cbp-pgcontainer">
	<ul class="cbp-pggrid">
		<li>
			<div class="cbp-pgcontent">
				<span class="cbp-pgrotate">Rotate Item</span>
				<div class="cbp-pgitem">
					<div class="cbp-pgitem-flip">
						<img src="images/1_front.png" />
						<img src="images/1_back.png" />
					</div>
				</div><!-- /cbp-pgitem -->
				<ul class="cbp-pgoptions">
					<li class="cbp-pgoptcompare">Compare</li>
					<li class="cbp-pgoptfav">Favorite</li>
					<li class="cbp-pgoptsize">								
						<span data-size="XL">XL</span>
						<div class="cbp-pgopttooltip">
							<span data-size="XL">XL</span>
							<span data-size="L">L</span>
							<span data-size="M">M</span>
							<span data-size="S">S</span>
						</div>
					</li>
					<li class="cbp-pgoptcolor">
						<span data-color="c1">Blue</span>
						<div class="cbp-pgopttooltip">
							<span data-color="c1">Blue</span>
							<span data-color="c2">Pink</span>
							<span data-color="c3">Orange</span>
							<span data-color="c4">Green</span>
						</div>
					</li>
					<li class="cbp-pgoptcart"></li>
				</ul><!-- cbp-pgoptions -->
			</div><!-- cbp-pgcontent -->
			<div class="cbp-pginfo">
				<h3>Save my trees</h3>
				<span class="cbp-pgprice">$29</span>
			</div>
		</li>
		<li>
			<!-- ... -->
		</li>
		<li>
			<!-- ... -->
		</li>
	</ul><!-- /cbp-pggrid -->
</div><!-- /cbp-pgcontainer -->

The CSS

@font-face {
	font-family: 'pgicons';
	src:url('../fonts/pgicons/pgicons.eot');
	src:url('../fonts/pgicons/pgicons.eot?#iefix') format('embedded-opentype'),
		url('../fonts/pgicons/pgicons.woff') format('woff'),
		url('../fonts/pgicons/pgicons.ttf') format('truetype'),
		url('../fonts/pgicons/pgicons.svg#pgicons') format('svg');
	font-weight: normal;
	font-style: normal;
}

.cbp-pgcontainer {
	position: relative;
	width: 100%;
	padding: 0 30px 100px 30px;
}

.cbp-pgcontainer ul,
.cbp-pgcontainer li {
	padding: 0;
	margin: 0;
	list-style-type: none;
}

.cbp-pggrid {
	position: relative;
	text-align: center;
}
/* If flexbox is supported we'll use it to lay out the grid */
.flexbox .cbp-pggrid {
	display: -webkit-flex;
	display: -moz-flex;
	display: -ms-flex;
	display: flex;
	-webkit-flex-flow: row wrap;
	-moz-flex-flow: row wrap;
	-ms-flex-flow: row wrap;
	flex-flow: row wrap;
	-webkit-justify-content: center;
	-moz-justify-content: center;
	-ms-justify-content: center;
	-webkit-justify-content: center;
}

.cbp-pggrid > li {
	display: inline-block;
	vertical-align: top;
	position: relative;
	width: 33%;
	min-width: 340px;
	max-width: 555px;
	padding: 20px 2% 50px 2%;
	text-align: left;
}

.flexbox .cbp-pggrid > li {
	display: block;
}

.cbp-pgcontent {
	border: 3px solid #47a3da;
	position: relative;
}

.cbp-pgrotate {
	width: 36px;
	height: 36px;
	position: absolute;
	display: block;
	color: transparent;
	font-size: 0;
	z-index: 100;
	border-bottom: 3px solid #47a3da;
	border-left: 3px solid #47a3da;
	right: 0px;
	top: 0px;
	cursor: pointer;
	text-align: center;
}

.cbp-pgrotate:before {
	font-size: 18px;
	line-height: 32px;
	color: #47a3da;
}

.no-touch .cbp-pgrotate:hover,
.cbp-pgrotate.cbp-pgrotate-active {
	background: #47a3da;
}

.no-touch .cbp-pgrotate:hover:before,
.cbp-pgrotate.cbp-pgrotate-active:before {
	color: #fff;
}

/* The item with the images will have perspective */
.cbp-pgitem {
	width: 100%;
	position: relative;
	padding: 2em;
	-webkit-perspective: 1400px;
	-moz-perspective: 1000px;
	perspective: 1000px;
	-webkit-backface-visibility: hidden;
	-moz-backface-visibility: hidden;
	backface-visibility: hidden;
}

/* The flip container */
.cbp-pgitem-flip {
	-webkit-transform-style: preserve-3d;
	-moz-transform-style: preserve-3d;
	transform-style: preserve-3d;
	-webkit-transition: -webkit-transform .4s ease-out;
	-moz-transition: -moz-transform .4s ease-out;
	transition: transform .4s ease-out;
}

/* If you want to rotate on hover instead of click, you could use something like this: 
.cbp-pgrotate:hover + .cbp-pgitem .cbp-pgitem-flip
*/
.cbp-pgitem.cbp-pgitem-showback .cbp-pgitem-flip {
	-webkit-transform: rotateY(180deg);
	-moz-transform: rotateY(180deg);
	transform: rotateY(180deg);
}

.cbp-pgitem-flip img {
	display: block;
	margin: 0 auto;
	max-width: 100%;
	max-height: 100%;
	-webkit-backface-visibility: hidden;
	-moz-backface-visibility: hidden;
	backface-visibility: hidden;
}

.cbp-pgitem img:first-child {
	position: relative;
}

/* The second image will be rotated so that we'd be looking at the back of it */
.cbp-pgitem img:nth-child(2) {
	position: absolute;
	top: 50%;
	left: 50%;
	-webkit-transform: translateX(-50%) translateY(-50%) rotateY(-180deg);
	-moz-transform: translateX(-50%) translateY(-50%) rotateY(-180deg);
	transform: translateX(-50%) translateY(-50%) rotateY(-180deg);
}

/* Fallback for browsers that don't support 3d transforms */
.no-csstransforms3d .cbp-pgitem img:nth-child(2) {
	position: relative;
	top: 0;
	left: 0;
	display: none;
}

.no-csstransforms3d .cbp-pgitem.cbp-pgitem-showback img:first-child {
	display: none;
}

.no-csstransforms3d .cbp-pgitem.cbp-pgitem-showback img:nth-child(2) {
	display: block;
}

/* The options bar */
.cbp-pgoptions {
	height: 60px;
	width: 100%;
 	border-top: 3px solid #47a3da;;
}

.cbp-pgoptions > li {
	width: 20%;
	height: 100%;
	float: left;
	position: relative;
	display: block;
	cursor: pointer;
	color: transparent;
	font-size: 0;
	border-left: 3px solid #47a3da;
	-webkit-touch-callout: none;
	-webkit-user-select: none;
	-khtml-user-select: none;
	-moz-user-select: none;
	-ms-user-select: none;
	user-select: none;
}

.cbp-pgoptions > li:first-child {
	border-left: none;
} 

.no-touch .cbp-pgoptions li {
	color: #47a3da;
}

.no-touch .cbp-pgoptions li:hover,
.cbp-pgoptions li.cbp-pgoption-active {
	background: #47a3da;
}

.cbp-pgoptions li:before,
.cbp-pgoptions li > span {
	font-size: 22px;
	line-height: 60px;
	text-indent: 0;
	text-align: center;
	color: #47a3da;
}

.no-touch .cbp-pgoptions li:hover:before,
.no-touch .cbp-pgoptions li:hover > span,
.cbp-pgoptions li.cbp-pgoption-active > span  {
	color: #fff;
}

.cbp-pgoptions li.cbp-pgoptsize > span {
	font-size: 22px;
}

.cbp-pgoptions li > span {
	display: block;
}

.cbp-pgoptions li:before {
	position: absolute;
	width: 100%;
	height: 100%;
}

/* Icons */
.cbp-pgoptcompare, 
.cbp-pgoptcart, 
.cbp-pgoptfav,
.cbp-pgrotate {
	font-family: 'pgicons';
	speak: none;
	font-style: normal;
	font-weight: normal;
	font-variant: normal;
	text-transform: none;
	line-height: 1;
	-webkit-font-smoothing: antialiased;
}

.cbp-pgoptcompare:before {
	content: "e001";
}

.cbp-pgoptfav:before {
	content: "e003";
}

.cbp-pgoptfav.cbp-pgoptfav-selected:before {
	content: "e002";
	color: #ee73b8;
}

.cbp-pgoptfav.cbp-pgoptfav-selected:hover:before {
	color: #f9c0e0;
}

.cbp-pgoptcart:before {
	content: "e000";
}

.cbp-pgrotate:before {
	content: "e004";
}

/* Tooltips */
.cbp-pgopttooltip {
	position: absolute;
	bottom: 180%;
	margin-bottom: 0px;
	background: #fff;
	padding: 25px;
	width: 100px;
	left: 50%;
	margin-left: -50px;
	border: 3px solid #47a3da;
	opacity: 0;
	z-index: 1000;
	visibility: hidden;
	pointer-events: none;
	-webkit-transition: visibility 0s 0.3s, opacity 0.3s, bottom 0.3s;
	-moz-transition: visibility 0s 0.3s, opacity 0.3s, bottom 0.3s;
	transition: visibility 0s 0.3s, opacity 0.3s, bottom 0.3s;
}

.cbp-pgoptions li:hover .cbp-pgopttooltip,
.cbp-pgoptions li.cbp-pgoption-active .cbp-pgopttooltip {
	visibility: visible;
	opacity: 1;
	-webkit-transition-delay: 0s;
	-moz-transition-delay: 0s;
	transition-delay: 0s;
	bottom: 100%;
	pointer-events: auto;
}

.cbp-pgopttooltip:after,
.cbp-pgopttooltip:before {
	top: 100%;
	border: solid transparent;
	content: " ";
	height: 0;
	width: 0;
	position: absolute;
	pointer-events: none;
}

.cbp-pgopttooltip:after {
	border-color: transparent;
	border-top-color: #fff;
	border-width: 10px;
	left: 50%;
	margin-left: -10px;
}

.cbp-pgopttooltip:before {
	border-color: transparent;
	border-top-color: #47a3da;
	border-width: 14px;
	left: 50%;
	margin-left: -14px;
}

/* Size tooltip */
.cbp-pgoptsize .cbp-pgopttooltip {
	margin-left: -50px;
}

.cbp-pgoptsize .cbp-pgopttooltip span {
	display: block;
	text-indent: 0;
	background: url(../images/tshirt.svg) no-repeat center center;
	background-size: 100%;
	margin: 0 auto 4px;
	text-align: center;
	font-size: 12px;
	font-weight: 700;
	color: #65b3e2;
}

.cbp-pgoptsize .cbp-pgopttooltip span:hover {
	color: #0968a1;
	-webkit-transform: scale(1.1);
	-moz-transform: scale(1.1);
	transform: scale(1.1);
}

.cbp-pgoptsize .cbp-pgopttooltip span[data-size="XL"] {
	width: 44px;
	height: 44px;
	line-height: 44px;
}

.cbp-pgoptsize .cbp-pgopttooltip span[data-size="L"] {
	width: 40px;
	height: 40px;
	line-height: 40px;
}

.cbp-pgoptsize .cbp-pgopttooltip span[data-size="M"] {
	width: 34px;
	height: 34px;
	line-height: 34px;
}

.cbp-pgoptsize .cbp-pgopttooltip span[data-size="S"] {
	width: 30px;
	height: 30px;
	line-height: 30px;
}

/* Color tooltip */
.cbp-pgoptcolor .cbp-pgopttooltip {
	padding: 5px;
}

.cbp-pgoptions li.cbp-pgoptcolor > span,
.cbp-pgoptcolor .cbp-pgopttooltip span {
	display: block;
	margin: 12px auto 0;
	text-indent: -900em;
}

.cbp-pgoptions li.cbp-pgoptcolor > span {
	width: 36px;
	height: 36px;
	border: 3px solid #fff;
}

.cbp-pgoptcolor .cbp-pgopttooltip span {
	float: left;
	margin: 4px;
	width: 34px;
	height: 34px;
}

.no-touch .cbp-pgoptcolor .cbp-pgopttooltip span:hover {
	-webkit-transform: scale(1.1);
	-moz-transform: scale(1.1);
	transform: scale(1.1);
}

.cbp-pgoptcolor span[data-color="c1"] {
	background: #72bbe9;
}

.cbp-pgoptcolor span[data-color="c2"] {
	background: #e577aa;
}

.cbp-pgoptcolor span[data-color="c3"] {
	background: #e5b178;
}

.cbp-pgoptcolor span[data-color="c4"] {
	background: #7abe93;
}

.cbp-pginfo {
	padding-top: 10px;
}

.cbp-pginfo:before,
.cbp-pginfo:after {
	content: " ";
	display: table;
}

.cbp-pginfo:after {
	clear: both;
}

.cbp-pginfo h3,
.cbp-pginfo span {
	float: left;
	width: 50%;
	font-size: 1.8em;
	padding: 10px 5px;
	margin: 0;
}

.cbp-pginfo h3 {
	font-weight: 300;
}

.cbp-pginfo span {
	font-weight: 700;
	text-align: right;
}
/* Media Queries */

@media screen and (max-width: 68.125em) {
	.cbp-pggrid > li {
		width: 48%;
	}
}

@media screen and (max-width: 46.125em) {
	.cbp-pggrid > li {
		width: 100%;
	}
}

Tiny break: 📬 Want to stay up to date with frontend and trends in web design? Subscribe and get our Collective newsletter twice a tweek.

The JavaScript

/**
 * cbpShop.js v1.0.0
 * http://www.codrops.com
 *
 * Licensed under the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 * 
 * Copyright 2013, Codrops
 * http://www.codrops.com
 */
;( function( window ) {
	
	'use strict';

	function cbpShop( el ) {
		this.el = el;
		this._init();
	}

	cbpShop.prototype = {
		_init : function() {
			var self = this;
			
			this.touch = Modernizr.touch;

			this.products = this.el.querySelectorAll( 'ul.cbp-pggrid > li' );
			Array.prototype.slice.call( this.products ).forEach( function( el, i ) {
				var content = el.querySelector( 'div.cbp-pgcontent' ),
					item = content.querySelector( 'div.cbp-pgitem' ),
					rotate = content.querySelector( 'span.cbp-pgrotate' );

				if( self.touch ) {

					rotate.addEventListener( 'touchstart', function() { self._rotateItem( this, item ); } );

					var options = content.querySelector( 'ul.cbp-pgoptions' ),
						size = options.querySelector( 'li.cbp-pgoptsize > span' ),
						color = options.querySelector( 'li.cbp-pgoptcolor > span' );
					
					size.addEventListener( 'touchstart', function() { self._showItemOptions( this ); } );
					color.addEventListener( 'touchstart', function() { self._showItemOptions( this ); } );
				}
				else {
					rotate.addEventListener( 'click', function() { self._rotateItem( this, item ); } );
				}
			} );
		},
		_rotateItem : function( trigger, item ) {
			if( item.getAttribute( 'data-open' ) === 'open' ) {
				item.setAttribute( 'data-open', '' );
				trigger.className = trigger.className.replace(/b cbp-pgrotate-activeb/,'');
				item.className = item.className.replace(/b cbp-pgitem-showbackb/,'');
			}
			else {
				item.setAttribute( 'data-open', 'open' );
				trigger.className += ' cbp-pgrotate-active';
				item.className += ' cbp-pgitem-showback';
			}
		},
		_showItemOptions : function( trigger ) {
			if( trigger.getAttribute( 'data-open' ) === 'open' ) {
				trigger.setAttribute( 'data-open', '' );
				trigger.parentNode.className = trigger.parentNode.className.replace(/b cbp-pgoption-activeb/,'');
			}
			else {
				trigger.setAttribute( 'data-open', 'open' );
				trigger.parentNode.className += ' cbp-pgoption-active';
			}
		},
		/*
		other functions..
		*/
	}

	window.cbpShop = cbpShop;

} )( window );

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 in the loop: Get your dose of frontend twice a week

Fresh news, inspo, code demos, and UI animations—zero fluff, all quality. Make your Mondays and Thursdays creative!

Feedback 22

Comments are closed.
  1. Your designs are absolutely wonderful, thank you so much for sharing!

  2. wow.. its really nice and simple. love it… thank you for sharing..

  3. Hi,

    I got javascript object error (Jscript object is expected on line 11) on IE8 (windows 7 starter)
    Error details (sorry in dutch) are below

    Foutdetails webpagina

    Gebruikersagent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0; Tablet PC 2.0)
    Tijdstempel: Sat, 18 May 2013 23:07:02 UTC

    Bericht: JScript-object wordt verwacht
    Regel: 11
    Teken: 180
    Code: 0
    URI: http://tympanus.net/Blueprints/ProductGridLayout/js/cbpShop.min.js

  4. I’m having problem with the demo, the tooltip perfectly shown up when i hover it, but when i click any button (the rotate and the others) nothing happen. I’m using Chrome Version 27.0.1453.93 m , on Windows 8. Thanks by the way, and absolutely great work!

  5. Love the template – really useful and great design. I’m struggling to turn the cart and heart icons into a link – any pointers? I’d like the user to be able to click on the icons as a link. Thanks!

  6. This used to work on FF a while back but doesn’t anymore. Reason is that you mordernizr detect’s ff’s touch feature and hence the script runs the code to listen to touch events instead of click events.

    So what i did was i did a little digging to find an html5 property which gets detected by FF but not on mobile devices and i found flexbox to be one of them. (Read: http://html5please.com/#flexbox). And i wrote some script to have the problem fixed. Replace the following code with the line # 32 to 45 in cbpShop.js to have your problem fixed:

    // Detecting Browser Touch events if( self.touch) { // Temp Mozilla FF Fix for Touch Events if ( jQuery('html').hasClass('flexbox') ) { rotate.addEventListener( 'click', function() { self._rotateItem( this, item ); } ); } // Touch Devices else { rotate.addEventListener( 'touchstart', function() { self._rotateItem( this, item ); } ); var options = content.querySelector( 'ul.cbp-pgoptions' ), size = options.querySelector( 'li.cbp-pgoptsize > span' ), color = options.querySelector( 'li.cbp-pgoptcolor > span' ); size.addEventListener( 'touchstart', function() { self._showItemOptions( this ); } ); color.addEventListener( 'touchstart', function() { self._showItemOptions( this ); } ); } } else { rotate.addEventListener( 'click', function() { self._rotateItem( this, item ); } ); }

    Please note I’m using jQuery to detect flexbox class on html element, I’m sure there’s a better way by using mordernizr’s api. Also this is a temporary fix until mobile devices start supporting flexbox. But i’m sure mordernizr would update soon to have this fixed.

  7. what if there’s like uneven numbers such as 5 products, the product at the bottom by itself is in the center instead of left. So I added float: left, and it works, but I noticed that the products are slightly to the left instead of being centered. Is there anyway I can fix it?