Filterable Product Grid

A responsive product grid layout with touch-friendly Flickity galleries and Isotope-powered filter functionality.

This Blueprint is a responsive Isotope-powered product grid layout where each grid item is a Flickity image slider. A small cart icon animation indicates that a product was added to the shopping cart. The product filter utilizes the Isotope filter functionality. Some example media queries are used to show how to make the layout adaptive.

Browser Support:
  • ChromeSupported
  • FirefoxSupported
  • Internet ExplorerSupported from version 9+
  • SafariSupported
  • OperaSupported

(Note that IE9 does not support animations.)


Flickity by David DeSandro used under the terms of the GNU GPL license v3. If you plan to use this resource commercially, please read more about Flickity’s license.

Isotope by David DeSandro used under the terms of the GNU GPL license v3. If you plan to use this resource commercially, please read more about Isotope’s license.

The HTML

<!-- Bottom bar with filter and cart info -->
<div class="bar">
    <div class="filter">
        <span class="filter__label">Filter: </span>
        <button class="action filter__item filter__item--selected" data-filter="*">All</button>
        <button class="action filter__item" data-filter=".jackets">
            <i class="icon icon--jacket"></i>
            <span class="action__text">Jackets</span>
        </button>
        <button class="action filter__item" data-filter=".shirts">
        	<i class="icon icon--shirt"></i>
        	<span class="action__text">Shirts</span>
        </button>
        <button class="action filter__item" data-filter=".dresses">
        	<i class="icon icon--dress"></i>
        	<span class="action__text">Dresses</span>
        </button>
        <button class="action filter__item" data-filter=".trousers">
        	<i class="icon icon--trousers"></i>
        	<span class="action__text">Trousers</span>
        </button>
        <button class="action filter__item" data-filter=".shoes">
        	<i class="icon icon--shoe"></i>
        	<span class="action__text">Shoes</span>
        </button>
    </div>
    <button class="cart">
        <i class="cart__icon fa fa-shopping-cart"></i>
        <span class="text-hidden">Shopping cart</span>
        <span class="cart__count">0</span>
    </button>
</div>
<!-- Main view -->
<div class="view">
    <!-- Grid -->
    <section class="grid">
	<!-- Loader -->
	<img class="grid__loader" src="images/grid.svg" width="60" alt="Loader image" />
        <!-- Grid sizer for a fluid Isotope (Masonry) layout -->
        <div class="grid__sizer"></div>
        <!-- Grid items -->
        <div class="grid__item shirts">
            <div class="slider">
                <div class="slider__item"><img src="images/product1/1.png" alt="product1_1" /></div>
                <div class="slider__item"><img src="images/product1/2.png" alt="product1_2" /></div>
                <div class="slider__item"><img src="images/product1/3.png" alt="product1_3" /></div>
            </div>
            <div class="meta">
                <h3 class="meta__title">Miriam Classic</h3>
                <span class="meta__brand">Miriam</span>
                <span class="meta__price">$79</span>
            </div>
            <button class="action action--button action--buy">
                <i class="fa fa-shopping-cart"></i>
                <span class="text-hidden">Add to cart</span>
            </button>
        </div>
        <div class="grid__item grid__item--size-a jackets">
            <!-- ... -->
        </div>
        <div class="grid__item shoes">
            <!-- ... -->
        </div>
        <div class="grid__item dresses">
            <!-- ... -->
        </div>
        <div class="grid__item trousers">
            <!-- ... -->
        </div>
    </section>
    <!-- /grid-->
</div>
<!-- /view -->
<script src="js/isotope.pkgd.min.js"></script>
<script src="js/flickity.pkgd.min.js"></script>
<script src="js/main.js"></script>

The CSS

/* Product grid */

.grid {
	position: relative;
	overflow: hidden;
	max-width: 1300px;
	margin: 0 auto;
	padding: 1.5em 0 8em;
	text-align: center;
}

/* Loader */
.grid__loader {
	display: none;
	margin: 3em auto 0;
}

.grid--loading .grid__loader {
	display: block;
}

/* Clearfix */

.grid:after {
	content: '';
	display: block;
	clear: both;
}

/* Grid items */

.grid__sizer,
.grid__item {
	position: relative;
	float: left;
	width: 20%;
	padding: .75em;
}

.no-touch .grid__sizer,
.no-touch .grid__item {
	padding: .75em .75em 1.25em;
}

.grid--loading .grid__item {
	visibility: hidden;
}

.grid__item--size-a {
	width: 40%;
}

/* Gallery */

.slider {
	padding: 0;
	border-radius: 5px;
	background: #24252a;
}

.no-touch .slider {
	padding: 0 0 1.25em;
}

.slider__item {
	width: 100%;
	padding: 1em;
}

.slider__item img {
	width: 100%;
}
/* Flickity page dots */

.slider .flickity-page-dots {
	bottom: 20px;
	opacity: 0;
	-webkit-transition: opacity .3s;
	transition: opacity .3s;
}

.no-touch .slider:hover .flickity-page-dots {
	opacity: 1;
}

.slider .flickity-page-dots .dot {
	background: #131417;
}

/* Product meta */

.meta {
	position: relative;
	margin: 10px 0 0;
	padding: 0 60px 0 0;
	text-align: left;
}

.meta__brand {
	font-size: .85em;
	font-weight: bold;
	display: block;
	color: #595b64;
}

.meta__title {
	font-size: .95em;
	font-weight: bold;
	margin: 0;
	padding: .4em 0 .1em;
}

.meta__price {
	font-size: .95em;
	font-weight: bold;
	position: absolute;
	top: .45em;
	right: .25em;
	color: #595b64;
}

/* Action style */

.action {
	font-family: Avenir, 'Helvetica Neue', 'Lato', 'Segoe UI', Helvetica, Arial, sans-serif;
	font-size: 1.05em;
	position: relative;
	overflow: hidden;
	margin: 0;
	padding: .25em;
	cursor: pointer;
	color: #fff;
	border: none;
	background: none;
}

.action:focus {
	outline: none;
}

.action--button {
	color: #5c5edc;
}

.no-touch .action--button:hover {
	color: #fff;
	outline: none;
}

.text-hidden {
	position: absolute;
	top: 200%;
}

/* Add to cart button */

.action--buy {
	position: absolute;
	top: 0;
	right: 0;
	padding: 1.85em 2.35em;
	-webkit-transition: opacity .3s, -webkit-transform .3s;
	transition: opacity .3s, transform .3s;
	-webkit-transform: translate3d(-5px, 0, 0);
	transform: translate3d(-5px, 0, 0);
}

.no-touch .action--buy {
	opacity: 0;
}

.no-touch .grid__item:hover .action--buy {
	opacity: 1;
	-webkit-transform: translate3d(0, 0, 0);
	transform: translate3d(0, 0, 0);
}

/* Fixed bottom bar */

.bar {
	position: fixed;
	z-index: 100;
	bottom: 0;
	left: 0;
	width: 100%;
	padding: 1.75em 5em;
	text-align: center;
	background: #191a1b;
	-webkit-transform: translate3d(0, 0, 0);
	/* Fix for Chrome flicker on Mac ...party like we're in 2012! */
}

.flexbox .filter {
	display: -webkit-flex;
	display: flex;
	-webkit-align-items: center;
	align-items: center;
	-webkit-justify-content: center;
	justify-content: center;
}

/* Filter */

.filter__label {
	font-size: .85em;
	display: inline-block;
	margin: 0 2%;
	font-weight: bold;
	color: #393A3F;
}

.filter__item {
	font-weight: bold;
	margin: 0 2%;
	padding: .1em;
	vertical-align: middle;
	color: #a3a3b3;
	border-bottom: 2px solid transparent;
}

.filter__item--selected {
	color: #5c5edc;
	border-color: #5c5edc;
}

.filter__item .icon {
	font-size: 1.75em;
	display: none;
}

/* Shopping cart */

.cart {
	font-size: 1.5em;
	position: absolute;
	top: 0;
	right: 0;
	overflow: hidden;
	height: 100%;
	padding: 0 1.195em;
	cursor: pointer;
	color: #abacae;
	border: none;
	background-color: #131415;
}

.no-touch .cart:focus,
.no-touch .cart:hover {
	color: #fff;
	outline: none;
}

.cart--animate .cart__icon {
	-webkit-animation: cartAnim .4s forwards;
	animation: cartAnim .4s forwards;
}

@-webkit-keyframes cartAnim {
	50% {
		opacity: 0;
		-webkit-transform: translate3d(50px, 0, 0);
		transform: translate3d(50px, 0, 0);
	}
	51% {
		opacity: 0;
		-webkit-transform: translate3d(-50px, 0, 0);
		transform: translate3d(-50px, 0, 0);
	}
	100% {
		opacity: 1;
		-webkit-transform: translate3d(0, 0, 0);
		transform: translate3d(0, 0, 0);
	}
}

@keyframes cartAnim {
	50% {
		opacity: 0;
		-webkit-transform: translate3d(50px, 0, 0);
		transform: translate3d(50px, 0, 0);
	}
	51% {
		opacity: 0;
		-webkit-transform: translate3d(-50px, 0, 0);
		transform: translate3d(-50px, 0, 0);
	}
	100% {
		opacity: 1;
		-webkit-transform: translate3d(0, 0, 0);
		transform: translate3d(0, 0, 0);
	}
}

.cart__count {
	font-size: 9px;
	font-weight: bold;
	line-height: 15px;
	position: absolute;
	top: 50%;
	right: 20px;
	width: 15px;
	height: 15px;
	margin: -16px 0 0 0;
	text-align: center;
	color: #fff;
	border-radius: 50%;
	background: #5c5edc;
}

.cart--animate .cart__count {
	-webkit-animation: countAnim .4s forwards;
	animation: countAnim .4s forwards;
}

@-webkit-keyframes countAnim {
	50% {
		opacity: 0;
		-webkit-transform: translate3d(0, 80px, 0);
		transform: translate3d(0, 80px, 0);
	}
	51% {
		opacity: 0;
		-webkit-transform: translate3d(0, -80px, 0);
		transform: translate3d(0, -80px, 0);
	}
	100% {
		opacity: 1;
		-webkit-transform: translate3d(0, 0, 0);
		transform: translate3d(0, 0, 0);
	}
}

@keyframes countAnim {
	50% {
		opacity: 0;
		-webkit-transform: translate3d(0, 80px, 0);
		transform: translate3d(0, 80px, 0);
	}
	51% {
		opacity: 0;
		-webkit-transform: translate3d(0, -80px, 0);
		transform: translate3d(0, -80px, 0);
	}
	100% {
		opacity: 1;
		-webkit-transform: translate3d(0, 0, 0);
		transform: translate3d(0, 0, 0);
	}
}
/* Resize grid items on smaller screens */

@media screen and (max-width: 65em) {
	.grid__sizer,
	.grid__item,
	.grid__item--size-a {
		width: 33.333%;
	}
}

@media screen and (max-width: 50em) {
	.grid__sizer,
	.grid__item,
	.grid__item--size-a {
		width: 50%;
	}
	.bar {
		padding-left: 0;
		text-align: left;
	}
}

@media screen and (max-width: 40em) {
	.bar {
		padding: .5em 4.5em .5em 0;
	}
	.flexbox .filter {
		-webkit-justify-content: space-around;
		justify-content: space-around;
	}
	.filter__item {
		height: 100%;
		padding: .5em .1em;
		border: none;
	}
	.filter__item .icon {
		display: inline-block;
	}
	.filter__label,
	.action__text {
		display: none;
	}
	.cart {
		padding: 0 1em;
	}
}

@media screen and (max-width: 25em) {
	.grid {
		max-width: 75%;
	}
	.grid__loader {
		margin: 0 auto;
	}
	.grid__sizer,
	.grid__item,
	.grid__item--size-a {
		width: 100%;
	}
	.action--buy {
		font-size: 1.5em;
		padding: 1.15em 1.5em;
		-webkit-tap-highlight-color: transparent;
	}
}

The JavaScript

/**
 * main.js
 * http://www.codrops.com
 *
 * Licensed under the MIT license.
 * http://www.opensource.org/licenses/mit-license.php
 * 
 * Copyright 2015, Codrops
 * http://www.codrops.com
 */
;(function(window) {

	'use strict';

	var support = { animations : Modernizr.cssanimations },
		animEndEventNames = { 'WebkitAnimation' : 'webkitAnimationEnd', 'OAnimation' : 'oAnimationEnd', 'msAnimation' : 'MSAnimationEnd', 'animation' : 'animationend' },
		animEndEventName = animEndEventNames[ Modernizr.prefixed( 'animation' ) ],
		onEndAnimation = function( el, callback ) {
			var onEndCallbackFn = function( ev ) {
				if( support.animations ) {
					if( ev.target != this ) return;
					this.removeEventListener( animEndEventName, onEndCallbackFn );
				}
				if( callback && typeof callback === 'function' ) { callback.call(); }
			};
			if( support.animations ) {
				el.addEventListener( animEndEventName, onEndCallbackFn );
			}
			else {
				onEndCallbackFn();
			}
		};

	// from http://www.sberry.me/articles/javascript-event-throttling-debouncing
	function throttle(fn, delay) {
		var allowSample = true;

		return function(e) {
			if (allowSample) {
				allowSample = false;
				setTimeout(function() { allowSample = true; }, delay);
				fn(e);
			}
		};
	}

	// sliders - flickity
	var sliders = [].slice.call(document.querySelectorAll('.slider')),
		// array where the flickity instances are going to be stored
		flkties = [],
		// grid element
		grid = document.querySelector('.grid'),
		// isotope instance
		iso,
		// filter ctrls
		filterCtrls = [].slice.call(document.querySelectorAll('.filter > button')),
		// cart
		cart = document.querySelector('.cart'),
		cartItems = cart.querySelector('.cart__count');

	function init() {
		// preload images
		imagesLoaded(grid, function() {
			initFlickity();
			initIsotope();
			initEvents();
			classie.remove(grid, 'grid--loading');
		});
	}

	function initFlickity() {
		sliders.forEach(function(slider){
			var flkty = new Flickity(slider, {
				prevNextButtons: false,
				wrapAround: true,
				cellAlign: 'left',
				contain: true,
				resize: false
			});

			// store flickity instances
			flkties.push(flkty);
		});
	}

	function initIsotope() {
		iso = new Isotope( grid, {
			isResizeBound: false,
			itemSelector: '.grid__item',
			percentPosition: true,
			masonry: {
				// use outer width of grid-sizer for columnWidth
				columnWidth: '.grid__sizer'
			},
			transitionDuration: '0.6s'
		});
	}

	function initEvents() {
		filterCtrls.forEach(function(filterCtrl) {
			filterCtrl.addEventListener('click', function() {
				classie.remove(filterCtrl.parentNode.querySelector('.filter__item--selected'), 'filter__item--selected');
				classie.add(filterCtrl, 'filter__item--selected');
				iso.arrange({
					filter: filterCtrl.getAttribute('data-filter')
				});
				recalcFlickities();
				iso.layout();
			});
		});

		// window resize / recalculate sizes for both flickity and isotope/masonry layouts
		window.addEventListener('resize', throttle(function(ev) {
			recalcFlickities()
			iso.layout();
		}, 50));

		// add to cart
		[].slice.call(grid.querySelectorAll('.grid__item')).forEach(function(item) {
			item.querySelector('.action--buy').addEventListener('click', addToCart);
		});
	}

	function addToCart() {
		classie.add(cart, 'cart--animate');
		setTimeout(function() {cartItems.innerHTML = Number(cartItems.innerHTML) + 1;}, 200);
		onEndAnimation(cartItems, function() {
			classie.remove(cart, 'cart--animate');
		});
	}

	function recalcFlickities() {
		for(var i = 0, len = flkties.length; i < len; ++i) {
			flkties[i].resize();
		}
	}

	init();

})(window);

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 30

Comments are closed.
  1. It’s amazing, I also had a similar (not as awesome) animation with Mixitup though its incompatibility with masonry was an impediment.

  2. Nice article! But.. Why Flickity and not Slick(not advertisement)?

    Flickity is a commercial for developers(ThemeForest, for example). Slick is fully open source and really awesome in my opinion. Take a look on that 🙂

    • Seriously? It’s $25 per developer/unlimited commercial projects, but for open-source projects it’s GPLv3!

      Methinks you doth complain too much..

  3. Wow nice Mary!
    But i think this cart-animation when adding a new product is too much.
    Maybe it could just animate the numbers or let the cart shake a little… I think this way it generates a good attention too

  4. Very nice article. Thanks for sharing it, I am new to website design and development and I love your work it is so professional.

  5. This is really cool. I downloaded the source code and tried to edit it (I’m trying to make a sortable game library) in Dreamweaver, but when I modify anything with the layout (removing add to cart button most consistently causes this), and refresh the preview, it gets stuck on loading. Most consistent method for recreating this is commenting out ln 73 in index.html. Does anyone have any ideas?

    • If you want to remove add cart button and evade loading problem you need go to “main.js” to line 119 and comment this

      [].slice.call(grid.querySelectorAll('.grid__item')).forEach(function(item) { item.querySelector('.action--buy').addEventListener('click', addToCart); });

    • And if you want implement this gallery to your site as a simple gallery without cart option, don’t forget to comment this line in main.js (58 line in code)
      cart = document.querySelector('.cart'), cartItems = cart.querySelector('.cart__count');

  6. The right column on mobile (iPhone 6+) suddenly slides out of frame and back in when scrolling up or down.

    Haven’t looked at the code yet but has anyone noticed and found a solution?

  7. Mary Lou I Love U! ???? I’ve learned a lot! Thank you for your preciosa time and efforts!

  8. I love this grid!

    I just wonder how can I change the filter buttons with multiple dropdown select. I’ve tried the following code but didn’t work.
    function initEvents() {
    filterCtrls.forEach(function(filterCtrl) {
    filterCtrl.addEventListener(‘change’, function() {
    var filterValue = this.value;
    classie.remove(filterCtrl.parentNode.querySelector(‘.filter__item–selected’), ‘filter__item–selected’);
    classie.add(filterCtrl, ‘filter__item–selected’);
    iso.arrange({
    filter: filterCtrl.filterValue
    });
    recalcFlickities();
    iso.layout();
    });
    });

    • Hi Mike,
      I have achieved the same functionality with multiple dropdown. I have forked FilterableProductGrid repo and will shortly pushed it.

      Thank you.

  9. Hello, I have tried to put this grid into an asp.net page, but when I click filter buttons, it causes a refresh (postback) and selection return to ‘All’

  10. Love it!

    I’m trying to tweak this so that I can send an event like $carousel.flickity( ‘select’, index ); to one of the instances of flickity in this.

    I can get a result from: Flickity.data($carousel).selectedIndex — so I know that the flickity initialization works . . . I’m just having trouble accessing it using the .flickity() function.

    basically, my use case here is that you could offer a “green” selector and sort using isotope (if you add the right classes) but I would like the flickity instances to snap to the matching color product too.

  11. i am trying to all images are load to json file so i have json file load succfully but this data are load properly in chrome but dose not work in fire fox pls’ help

  12. I only want to load a specific category products on first load and not all the products. What could be the possible workaround for this ?