Product Comparison Layout & Effect

A basic responsive product grid layout with comparison functionality and a slide-in effect.
BlueprintProductComparison

From our weekly sponsor: Design every part of your website with the brand new Divi Theme Builder. Try it for free.

This Blueprint is a responsive product grid layout with comparison functionality. A maximum of three items can be selected for the product comparison. The comparison view shows flexbox-powered columns or rows (depending on the viewport size) that appear with a slide-in effect. There are a couple of example media queries for smaller viewports.

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

The HTML

<!-- Compare basket -->
<div class="compare-basket">
	<!-- comparison item miniatures come here -->
	<button class="action action--button action--compare"><i class="fa fa-check"></i><span class="action__text">Compare</span></button>
</div>
<!-- Main view -->
<div class="view">
	<!-- Blueprint header -->
	<header class="bp-header cf"><!-- ... --></header>
	<!-- Product grid -->
	<section class="grid">
		<!-- Products -->
		<div class="product">
			<div class="product__info">
				<img class="product__image" src="images/1.png" alt="Product 1" />
				<h3 class="product__title">Chryseia</h3>
				<span class="product__year extra highlight">2011</span>
				<span class="product__region extra highlight">Douro</span>
				<span class="product__varietal extra highlight">Touriga Nacional</span>
				<span class="product__alcohol extra highlight">13%</span>
				<span class="product__price highlight">$55.90</span>
				<button class="action action--button action--buy">
					<i class="fa fa-shopping-cart"></i>
					<span class="action__text">Add to cart</span>
				</button>
			</div>
			<label class="action action--compare-add">
				<input class="check-hidden" type="checkbox" />
				<i class="fa fa-plus"></i>
				<i class="fa fa-check"></i>
				<span class="action__text action__text--invisible">Add to compare</span>
			</label>
		</div>
		<div class="product">
			<!-- ... -->
		</div>
		<div class="product">
			<!-- ... -->		
		</div>
		<!-- ... -->	
		</section>
</div><!-- /view -->
<!-- product compare wrapper -->
<section class="compare">
	<!-- comparison items come here -->
	<button class="action action--close"><i class="fa fa-remove"></i><span class="action__text action__text--invisible">Close comparison overlay</span></button>
</section>
<script src="js/classie.js"></script>
<script src="js/main.js"></script>

The CSS

.view {
	-webkit-transition: -webkit-transform 0.4s ease-in-out;
	transition: transform 0.4s ease-in-out;
}

.view--compare {
	-webkit-transform: scale3d(0.9,0.9,1);
	transform: scale3d(0.9,0.9,1);
}

/* product grid */
.grid {
	margin: 0 auto;
	padding: 4em 1em;
	max-width: 1200px;
	text-align: center;
	overflow: hidden;
	-webkit-touch-callout: none;
	-webkit-user-select: none;
	-khtml-user-select: none;
	-moz-user-select: none;
	-ms-user-select: none;
	user-select: none;
}

/* if flexbox is supported, let's use it to lay out the products */
.flexbox .grid {
	display: -ms-flexbox;
	display: -webkit-flex;
	display: flex;
	-webkit-flex-direction: row;
	-ms-flex-direction: row;
	flex-direction: row;
	-webkit-flex-wrap: wrap;
	-ms-flex-wrap: wrap;
	flex-wrap: wrap;
	-webkit-justify-content: center;
	-ms-flex-pack: center;
	justify-content: center;
	-webkit-align-content: stretch;
	-ms-flex-line-pack: stretch;
	align-content: stretch;
	-webkit-align-items: flex-start;
	-ms-flex-align: start;
	align-items: flex-start;
}

/* product */
.product {
	position: relative;
	display: inline-block;
	vertical-align: top;
	min-width: 16em;
	margin: 0 1em 2.5em;
	padding: 1.5em 1.5em 2em;
	background: #24252A;
	border-radius: 5px;
}

.product--selected {
	box-shadow: 0 0 0 2px #5C5EDC;
}

.flexbox .product {
	display: block;
	-webkit-flex: 0 0 16em;
	-ms-flex: 0 0 16em;
	flex: 0 0 16em;
}

/* product info */
.product__info > span {
	display: block;
	padding: 1em 0;
}

/* since we'll be using the product info inside of the comparison, we'll hide the extra info for the grid view */
.grid .extra {
	display: none;
}

.product__image {
	display: block;
	margin: 0 auto;
	max-width: 100%;
	-webkit-transform-origin: 50% 100%;
	transform-origin: 50% 100%;
}

.product:hover .product__image {
	-webkit-animation: swing 0.6s forwards;	
	animation: swing 0.6s forwards;	
}

/* https://daneden.github.io/animate.css/ */
@-webkit-keyframes swing {
	25% {
		-webkit-transform: rotate3d(0, 0, 1, 6deg);
		transform: rotate3d(0, 0, 1, 6deg);
	}

	50% {
		-webkit-transform: rotate3d(0, 0, 1, -4deg);
		transform: rotate3d(0, 0, 1, -4deg);
	}

	75% {
		-webkit-transform: rotate3d(0, 0, 1, 2deg);
		transform: rotate3d(0, 0, 1, 2deg);
	}

	100% {
		-webkit-transform: rotate3d(0, 0, 1, 0deg);
		transform: rotate3d(0, 0, 1, 0deg);
	}
}

@keyframes swing {
  25% {
		-webkit-transform: rotate3d(0, 0, 1, 6deg);
		transform: rotate3d(0, 0, 1, 6deg);
	}

	50% {
		-webkit-transform: rotate3d(0, 0, 1, -4deg);
		transform: rotate3d(0, 0, 1, -4deg);
	}

	75% {
		-webkit-transform: rotate3d(0, 0, 1, 2deg);
		transform: rotate3d(0, 0, 1, 2deg);
	}

	100% {
		-webkit-transform: rotate3d(0, 0, 1, 0deg);
		transform: rotate3d(0, 0, 1, 0deg);
	}
}

.product__title {
	font-size: 150%;
	margin: 1em 0 0;
	min-height: 3em;
}

.product__price {
	font-weight: bold;
	color: #797BED;
}

.action {
	display: inline-block;
	font-size: 1em;
	white-space: nowrap;
	padding: 0.85em 1.25em;
	cursor: pointer;
	border: none;
	background: transparent;
	text-align: center;
}

.action:focus {
	outline: none;
}

.action--button {
	background: #2C2D34;
	color: #fff;
	border-radius: 2px;
	-webkit-transition: background 0.2s;
	transition: background 0.2s;
}

.action--button:hover {
	background: #5C5EDC;
}

.action__text {
	font-family: 'Raleway', 'Helvetica Neue', Helvetica, Arial, sans-serif;
	font-weight: bold;
	letter-spacing: 1px;
	font-size: .813em;
	vertical-align: middle;
	display: inline-block;
}

.action__text--invisible {
	position: absolute;
	top: 100%;
	opacity: 0;
	pointer-events: none;
}

.action--button i + span {
	margin-left: 1em;
}

.flexbox .action--buy {
	-webkit-align-self: center;
	-ms-flex-item-align: center;
	align-self: center;
	margin-top: 1em;
	-webkit-flex: none;
	-ms-flex: none;
	flex: none;
}

.action--close {
	position: absolute;
	overflow: hidden;
	top: 0;
	right: 0;
	font-size: 1.5em;
	color: #ddd;
	pointer-events: none;
	opacity: 0;
	-webkit-transition: opacity 0.3s, background 0.2s;
	transition: opacity 0.3s, background 0.2s;
}

.view--compare + .compare .action--close {
	pointer-events: auto;
	opacity: 1;
	-webkit-transition-delay: 0.4s, 0s;
	transition-delay: 0.4s, 0s;
}

.action--close:hover,
.action--close:focus {
	color: #797BED;
}

.action--compare {
	margin: 0 0 0 4px;
	opacity: 0;
	pointer-events: none;
	cursor: default;
	background-color: #34363D;
	color: #565B6C;
	-webkit-transition: opacity 0.3s;
	transition: opacity 0.3s;
}

.compare-basket--active .action--compare {
	opacity: 1;
}

.compare-basket--active .action--compare:nth-child(3),
.compare-basket--active .action--compare:nth-child(4) {
	background-color: #494BC7;
	color: #fff;
	pointer-events: auto;
	cursor: pointer;
}

.action--remove {
	position: absolute;
	overflow: hidden;
	color: #ddd;
	top: 0px;
	right: 2px;
	padding: 0;
	font-size: 0.65em;
}

.action--compare-add {
	color: #ddd;
	position: absolute;
	top: 10px;
	right: 5px;
}

.action--compare-add:hover .action__text--invisible {
	opacity: 1;
	top: 35px;
	right: 10px;
	color: #ddd;
	font-size: 75%;
	letter-spacing: 0;
	background: #2F3035;
	border-radius: 2px;
	padding: 3px 5px;
}

.action--remove:hover,
.action--compare-add:hover {
	color: #5C5EDC;
}

.action--compare-add .fa-check,
.action--compare-add input[type=checkbox]:checked ~ .fa-plus {
	display: none;
}

.action--compare-add input[type=checkbox]:checked ~ .fa-check {
	display: block;
	color: #5C5EDC;
}

.check-hidden {
	position: absolute;
	opacity: 0;
}

.compare-basket {
	width: 100%;
	padding: 0.75em;
	text-align: right;
	position: fixed;
	top: 0;
	left: 0;
	background: #212227;
	z-index: 1000;
	-webkit-touch-callout: none;
	-webkit-user-select: none;
	-khtml-user-select: none;
	-moz-user-select: none;
	-ms-user-select: none;
	user-select: none;
	-webkit-transform: translate3d(0,-100%,0);
	transform: translate3d(0,-100%,0);
	-webkit-transition: -webkit-transform 0.3s cubic-bezier(0.2,1,0.3,1);
	transition: transform 0.3s cubic-bezier(0.2,1,0.3,1);
}

.compare-basket--active {
	-webkit-transform: translate3d(0,0,0);
	transform: translate3d(0,0,0);
}

.flexbox .compare-basket {
	display: -webkit-flex;
	display: -ms-flex;
	display: flex;
	-webkit-justify-content: flex-end;
	-ms-flex-pack: end;
	justify-content: flex-end;
}

.product-icon {
	display: inline-block;
	vertical-align: middle;
	background: #42444F;
	width: 50px;
	height: 50px;
	padding: 5px;
	margin: 0 3px;
	border-radius: 2px;
	position: relative;
}

.product-icon::after {
	content: '';
	position: absolute;
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
	border-radius: 4px;
	z-index: -1;
	box-shadow: -56px 0 #2C2D34;
}

.compare-basket--full .product-icon::after {
	display: none;
}

.flexbox .product-icon {
	display: block;
}

/* comparison overlay */
.compare {
	position: fixed;
	z-index: 100;
	width: 100%;
	height: 0;
	overflow: hidden;
	top: 0;
	left: 0;
	z-index: 1001;
	-webkit-transition: height 0s 0.4s;
	transition: height 0s 0.4s;
}

.flexbox .compare {
	display: -webkit-flex;
	display: -ms-flex;
	display: flex;
}

.view--compare + .compare {
	pointer-events: auto;
	height: 100%;
	-webkit-transition: none;
	transition: none;
}

.compare::before {
	content: '';
	position: absolute;
	width: 100%;
	height: 100%;
	top: 0;
	left: 0;
	background: rgba(0,0,0,0.5);
	opacity: 0;
	-webkit-transition: opacity 0.4s;
	transition: opacity 0.4s;
}

.view--compare + .compare::before {
	opacity: 1;
}

.compare__item {
	height: 100%;
	width: 50%;
	background: #24252A;
	text-align: center;
	cursor: default;
	padding: 2em 0;
	-webkit-transition: -webkit-transform 0.4s ease-in-out;
	transition: transform 0.4s ease-in-out;
}

.no-flexbox .compare__item {
	display: inline-block;
	width: 50%;
}

.compare__item:nth-of-type(2) {
	background: #212227;
}

.compare__item .product__title {
	margin: 1em 0;
	min-height: 0;
}

.compare__item .product__price {
	color: #CECECE;
}

.compare__item span[class^="product__"] {
	display: block;
	padding: 0.85em 0;
	-webkit-transition: background-color 0.3s;
	transition: background-color 0.3s;
}

.compare__item span[class^="product__"].hover {
	background: #000;
}

/* three items */
.compare__item:first-of-type:nth-last-of-type(3),
.compare__item:first-of-type:nth-last-of-type(3) ~ .compare__item {
    width: 33.3333%;
}

.flexbox .compare__item {
	-webkit-flex: auto;
	-ms-flex: auto;
	flex: auto;
}

.compare__item:nth-child(odd) {
	-webkit-transform: translate3d(0,-100vh,0);
	transform: translate3d(0,-100vh,0);
}

.compare__item:nth-child(even) {
	-webkit-transform: translate3d(0,100vh,0);
	transform: translate3d(0,100vh,0);
}

.view--compare + .compare .compare__item:nth-child(odd),
.view--compare + .compare .compare__item:nth-child(even) {
	-webkit-transform: translate3d(0,0,0);
	transform: translate3d(0,0,0);
}

.compare__effect {
	width: 100%;
	height: 100%;
	opacity: 0;
	-webkit-transition: -webkit-transform 1s cubic-bezier(0.2, 1, 0.3, 1), opacity 1s cubic-bezier(0.2, 1, 0.3, 1);
	transition: transform 1s cubic-bezier(0.2, 1, 0.3, 1), opacity 1s cubic-bezier(0.2, 1, 0.3, 1);
}

.compare__item:nth-child(odd) .compare__effect {
	-webkit-transform: translate3d(0,-250px,0);
	transform: translate3d(0,-250px,0);
}

.compare__item:nth-child(even) .compare__effect {
	-webkit-transform: translate3d(0,250px,0);
	transform: translate3d(0,250px,0);
}

.view--compare + .compare .compare__item:nth-child(odd) .compare__effect,
.view--compare + .compare .compare__item:nth-child(even) .compare__effect {
	opacity: 1;
	-webkit-transform: translate3d(0,0,0);
	transform: translate3d(0,0,0);
	-webkit-transition-delay: 0.3s;
	transition-delay: 0.3s;
}

@media screen and (min-width: 59.688em) {
	.flexbox .compare__effect {
		display: -webkit-flex;
		display: -ms-flex;
		display: flex;
		-webkit-flex-direction: column;
		-ms-flex-direction: column;
		flex-direction: column;
		-webkit-justify-content: space-between;
		-ms-flex-pack: justify;
		justify-content: space-between;
	}

	.flexbox .compare__item .product__image {
		-webkit-align-self: center;
		-ms-flex-align: center;
		align-self: center;
		-webkit-flex: none;
		-ms-flex: none;
		flex: none;
	}
}

@media screen and (max-width: 59.688em) {
	.grid {
		padding: 2em 0.5em;
		font-size: 65%;
	}

	.product {
		margin: 0 0.5em 1em;
		min-width: 13em;
	}

	.product__title {
		font-size: 115%;
	}

	.flexbox .product {
		-webkit-flex: 0 0 13em;
		-ms-flex: 0 0 13em;
		flex: 0 0 13em;
	}

	.flexbox .compare {
		-webkit-flex-direction: column;
		-ms-flex-direction: column;
		flex-direction: column;
	}

	.no-flexbox .compare__item,
	.compare__item,
	.compare__item:first-of-type:nth-last-of-type(3),
	.compare__item:first-of-type:nth-last-of-type(3) ~ .compare__item {
		width: 100%;
	}

	.compare__item {
		text-align: left;
		padding: 1.5em;
		font-size: 90%;
	}

	.compare__item .product__image {
		height: 40px;
		float: left;
	}

	.compare__item .product__title {
		margin: 0 40px 0 43px;
		font-size: 0.85em;
		display: block;
	}

	.compare__item .product__year { border-bottom: 2px solid #2E294E; }
	.compare__item .product__region { border-bottom: 2px solid #6D6FD2; }
	.compare__item .product__varietal { border-bottom: 2px solid #4B5267; }
	.compare__item .product__alcohol { border-bottom: 2px solid #3C3474; }

	.action--close {
		padding: 0.5em 0.75em;
	}

	.compare__item .action--buy {
		margin: 0;
		display: block;
	}

	.compare__item span[class^="product__"] {
		display: inline-block;
		padding: 0.25em;
		margin: 0 0 0.5em 0;
		font-size: 0.85em;
	}

	.compare__item:nth-child(odd) {
		-webkit-transform: translate3d(-100%,0,0);
		transform: translate3d(-100%,0,0);
	}

	.compare__item:nth-child(even) {
		-webkit-transform: translate3d(100%,0,0);
		transform: translate3d(100%,0,0);
	}

	.compare__item:nth-child(odd) .compare__effect {
		-webkit-transform: translate3d(-250px,0,0);
		transform: translate3d(-250px,0,0);
	}

	.compare__item:nth-child(even) .compare__effect {
		-webkit-transform: translate3d(250px,0,0);
		transform: translate3d(250px,0,0);
	}
}

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() {

	var viewEl = document.querySelector('.view'),
		gridEl = viewEl.querySelector('.grid'),
		items = [].slice.call(gridEl.querySelectorAll('.product')),
		basket;

	// the compare basket
	function CompareBasket() {
		this.el = document.querySelector('.compare-basket');
		this.compareCtrl = this.el.querySelector('.action--compare');
		this.compareWrapper = document.querySelector('.compare'),
		this.closeCompareCtrl = this.compareWrapper.querySelector('.action--close')
		
		this.itemsAllowed = 3;
		this.totalItems = 0;
		this.items = [];

		// compares items in the compare basket: opens the compare products wrapper
		this.compareCtrl.addEventListener('click', this._compareItems.bind(this));
		// close the compare products wrapper
		var self = this;
		this.closeCompareCtrl.addEventListener('click', function() {
			// toggle compare basket
			classie.add(self.el, 'compare-basket--active');
			// animate..
			classie.remove(viewEl, 'view--compare');
		});
	}

	CompareBasket.prototype.add = function(item) {
		// check limit
		if( this.isFull() ) {
			return false;
		}

		classie.add(item, 'product--selected');

		// create item preview element
		var preview = this._createItemPreview(item);
		// prepend it to the basket
		this.el.insertBefore(preview, this.el.childNodes[0]);
		// insert item
		this.items.push(preview);

		this.totalItems++;
		if( this.isFull() ) {
			classie.add(this.el, 'compare-basket--full');
		}

		classie.add(this.el, 'compare-basket--active');
	};

	CompareBasket.prototype._createItemPreview = function(item) {
		var self = this;

		var preview = document.createElement('div');
		preview.className = 'product-icon';
		preview.setAttribute('data-idx', items.indexOf(item));
		
		var removeCtrl = document.createElement('button');
		removeCtrl.className = 'action action--remove';
		removeCtrl.innerHTML = '<i class="fa fa-remove"></i><span class="action__text action__text--invisible">Remove product</span>';
		removeCtrl.addEventListener('click', function() {
			self.remove(item);
		});
		
		var productImageEl = item.querySelector('img.product__image').cloneNode(true);

		preview.appendChild(productImageEl);
		preview.appendChild(removeCtrl);

		var productInfo = item.querySelector('.product__info').innerHTML;
		preview.setAttribute('data-info', productInfo);

		return preview;
	};

	CompareBasket.prototype.remove = function(item) {
		classie.remove(this.el, 'compare-basket--full');
		classie.remove(item, 'product--selected');
		var preview = this.el.querySelector('[data-idx = "' + items.indexOf(item) + '"]');
		this.el.removeChild(preview);
		this.totalItems--;

		var indexRemove = this.items.indexOf(preview);
		this.items.splice(indexRemove, 1);

		if( this.totalItems === 0 ) {
			classie.remove(this.el, 'compare-basket--active');
		}

		// checkbox
		var checkbox = item.querySelector('.action--compare-add > input[type = "checkbox"]');
		if( checkbox.checked ) {
			checkbox.checked = false;
		}
	};

	CompareBasket.prototype._compareItems = function() {
		var self = this;

		// remove all previous items inside the compareWrapper element
		[].slice.call(this.compareWrapper.querySelectorAll('div.compare__item')).forEach(function(item) {
			self.compareWrapper.removeChild(item);
		});

		for(var i = 0; i < this.totalItems; ++i) {
			var compareItemWrapper = document.createElement('div');
			compareItemWrapper.className = 'compare__item';

			var compareItemEffectEl = document.createElement('div');
			compareItemEffectEl.className = 'compare__effect';

			compareItemEffectEl.innerHTML = this.items[i].getAttribute('data-info');
			compareItemWrapper.appendChild(compareItemEffectEl);

			this.compareWrapper.insertBefore(compareItemWrapper, this.compareWrapper.childNodes[0]);
		}

		setTimeout(function() {
			// toggle compare basket
			classie.remove(self.el, 'compare-basket--active');
			// animate..
			classie.add(viewEl, 'view--compare');
		}, 25);
	};

	CompareBasket.prototype.isFull = function() {
		return this.totalItems === this.itemsAllowed;
	};

	function init() {
		// initialize an empty basket
		basket = new CompareBasket();
		initEvents();
	}

	function initEvents() {
		items.forEach(function(item) {
			var checkbox = item.querySelector('.action--compare-add > input[type = "checkbox"]');
			checkbox.checked = false;

			// ctrl to add to the "compare basket"
			checkbox.addEventListener('click', function(ev) {
				if( ev.target.checked ) {
					if( basket.isFull() ) {
						ev.preventDefault();
						return false;
					}
					basket.add(item);
				}
				else {
					basket.remove(item);
				}
			});
		});
	}

	init();

})();

Tagged with:

Mary Lou

ML is a freelance web designer and developer with a passion for interaction design. She studied Cognitive Science and Computational Logic and has a weakness for the smell of freshly ground peppercorns.

http://www.codrops.com

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

CSS Reference

Learn about all important CSS properties from the basics with our extensive and easy-to-read CSS Reference.

It doesn't matter if you are a beginner or intermediate, start learning CSS now.

Feedback 34

Comments are closed.
  1. The comparison bar doesn’t show up in Opera/Chrome. I’m guessing it has to do with this error that pops up in the console: Uncaught SyntaxError: Failed to execute 'querySelector' on 'Element': '.action--compare-add > input[type = "checkbox"]' is not a valid selector.

    In Firefox, it works fine and looks beautiful!

    • Works fine in Chrome 45 (Canary) but not in Chrome 43 (latest stable).
      Same error as Sander mentions.

      Looks fantastic in Chrome Canary though, nice job.

  2. Only issue I see here is that I can scroll while being in the comparison view. Some overflow: hidden would be great here.

  3. Really great demo, love some of the examples you put up on this site. Looking forward to implementing it in a new design I’m working on and seeing if it fits in.

  4. You might want to stop background scrolling when the compare view comes up.

  5. Mary Lou… I don’t know who you are. I don’t know what you want… But I will look for you, I will find you, and I will marry you. <3

  6. I don’t know a lot of HTML/CSS/JAVASCRIPT so, how I can put more than three items for the product comparison?

    • * In main.js, replace
      this.itemsAllowed = 3;
      with
      this.itemsAllowed = 4;

      * In component.css, replace
      .compare-basket-active .btn-compare:nth-child(3),
      .compare-basket-active .btn-compare:nth-child(4) {
      background-color: #494BC7;
      color: #fff;
      pointer-events: auto;
      cursor: pointer;
      }
      with
      .compare-basket-active .btn-compare:nth-child(3),
      .compare-basket-active .btn-compare:nth-child(4),
      .compare-basket-active .btn-compare:nth-child(5) {
      background-color: red;
      color: #fff;
      pointer-events: auto;
      cursor: pointer;
      }

  7. So amazingly awesome, great UI. Love the content too, as a former Sommelier it’s quite funny. Got a couple 2000 Branaire-Ducru in the cellar right now 🙂

  8. good Morning! Anyone know how to do to compare two specific items without having to add 1-1?

    Example: they have become the first click of the comparison to inves I go and pick 1-1

  9. How can I make the comparison bar inactive when clicking somewhere on the page?
    Currently it blocks the menu when 1 product is added to the comparison.