Recreating the “Design Samsung” Grid Loading Effect

A tutorial on how to achieve the grid loading effect seen on the “Design Samsung” site. The effect will show a colored element sliding in first and then sliding out again, revealing the image.

If you already come across the Samsung Corporate Design Center, you certainly have noticed the stylish grid loading effect. The colored background of an item slides in first and when it slides out again to the opposite side, the image is revealed. The color of the sliding element represents the image, i.e. it is colored in the dominant color of the picture. This is a great grid loading effect and after we got a request on how to achieve this, we’d like to show you how to recreate this effect using a Masonry grid with CSS animations. We’ll also make use of the ColorFinder script by pieroxy that will get the most prominent color of an image for us.

The idea is to load grid images showing a swiping animation of a colored element first. For that we’ll dynamically add a division that we’ll color with the most prominent color of the associated image and then we’ll animate that element to reveal the image. We’ll add an animation that looks like the one seen on the Samsung site and we’ll also add two more, a swipe to the bottom and a swipe rotation.

We’ll not load items or images dynamically, instead we’ll simulate their appearance on scroll. Of course, in real cases with dynamically loading content you might use something like lazy loading or infinite scrolling.

Note that this is for modern browsers only!

The beautiful artwork featured in the demos is by illustrator Ryo Takemasa. Check out his website, his Behance portfolio or his shop.

So, let’s get started!

The Markup

For our grid we’ll use an unordered list with a main wrapper. The first list item will have a special style, so we give it the class “title-box”:

<section class="grid-wrap">
	<ul class="grid swipe-right" id="grid">
		<li class="title-box">
			<h2>Illustrations by <a href="http://ryotakemasa.com/">Ryo Takemasa</a></h2>
		</li>
		<li><a href="#"><img src="img/1.jpg" alt="img01"><h3>Kenpo News April 2014 issue</h3></a></li>
		<li><a href="#"><img src="img/2.jpg" alt="img02"><h3>SQUET April 2014 issue</h3></a></li>
		<li><!-- ... --></li>
		<!-- ... -->
	</ul>
</section>

Each list item contains an anchor with an image and a title. Note that we’ll control which type of animation will be used by giving the unordered list one of the three classes swipe-right, swipe-down or swipe-rotate.

When loading the page, we will want the visible items to be shown already and then, when we scroll, we want to trigger our animation. This will be achieved by giving the class animate to the apparently loading list item. The initially visible items will get the class shown and so will the items that finished their animation.

The colored element that will swipe to reveal the image, will be added dynamically. We’ll use a div that we’ll insert into the anchor, right after the title. The division will have the class curtain and we’ll set the background color to the most prominent one that we discovered using the ColorFinder script.

Let’s take a look at the style.

The CSS

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

First, we need to style the main container which we’ll restrict to a maximum width of 1260 pixel (so that we fit a maximum of four items in a row):

.grid-wrap {
	clear: both;
	margin: 0 auto;
	padding: 0;
	max-width: 1260px;
}

The unordered list will be centered and we’ll remove the default list styling:

.grid {
	margin: 30px auto;
	padding: 0;
	min-height: 500px;
	list-style: none;
}

If we have JavaScript enabled, we want the grid visibility to be controlled by our script. We’ll use the class loaded, which we set once the grid is ready, to control a loading indicator and the visibility of the items.

.js .grid {
	background: url(../img/loading.gif) no-repeat 50% 100px;
}

.js .grid.loaded {
	background: none;
}

We do that so that we don’t show anything until the grid images are actually loaded.

The list items will float left and have a width of 314 pixel (image width plus the margin of the anchor) if we have JavaScript. If not, we’ll simply set the items as inline-block elements and align them to the top:

.grid li {
	display: inline-block;
	overflow: hidden;
	width: 314px;
	text-align: left;
	vertical-align: top;
}

.js .grid li {
	display: none;
	float: left;
}

.js .grid.loaded li {
	display: block;
}

Let’s give some special styling to the title:

.title-box h2 {
	display: block;
	margin: 7px;
	padding: 20px;
	background: #2E3444;
	color: #D3EEE2;
	text-transform: uppercase;
	letter-spacing: 1px;
	font-weight: 300;
}

.title-box h2 a {
	display: block;
	font-weight: 900;
}

.title-box h2 a:hover {
	color: #D3EEE2;
}

Let’s set some styles for the anchor and the image:

.grid li > a,
.grid li img {
	display: block;
	outline: none;
	border: none;
}

The anchor needs to have its overflow hidden because we want to move the colored element without it peeking out:

.grid li > a {
	position: relative;
	overflow: hidden;
	margin: 7px;
}

The curtain element will be positioned absolutely and we’ll set it to full width and height:

.grid .curtain {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	background: #96cdc8;
}

The curtain will be on top of the image and the title, which is important for our effect to work.

The three effects will have the curtain coming from either left, top or rotated from the left:

.grid.swipe-right .curtain {
	transform: translate3d(-100%,0,0);
}

.grid.swipe-down .curtain {
	transform: translate3d(0,-100%,0);
}

.grid.swipe-rotate .curtain {
	width: 200%;
	height: 200%;
	transform: rotate3d(0,0,1,90deg);
	transform-origin: top left;
}

For the rotate effect we’ll double the size of the element so that we don’t see any corners when the rotation is performed.

Additionally, we’ll attach a pseudo element to the curtain, which will serve as shadow that will cover the image. Depending on the effect, we’ll attach the element either on the left or above it:

.grid .curtain::after {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100%;
	background: rgba(0,0,0,1);
	content: '';
}

.grid.swipe-right .curtain::after,
.grid.swipe-rotate .curtain::after {
	left: -100%;
}

.grid.swipe-down .curtain::after {
	top: -100%;
}

The title will have a dark background color and it will be positioned absolutely:

.grid li h3 {
	position: absolute;
	bottom: 0;
	left: 0;
	margin: 0;
	padding: 20px;
	width: 100%;
	background: #2E3444;
	color: #D3EEE2;
	text-align: right;
	text-transform: uppercase;
	letter-spacing: 1px;
	font-weight: 800;
	font-size: 1em;
	transition: transform 0.2s, color 0.2s;
}

For the hover effect, we’ll play with the pseudo element ::before of the anchor. It will be a absolutely positioned element that will animate its border on hover. The title itself will move up a bit and change its color to white:

.grid li > a::before {
	position: absolute;
	top: 0;
	left: 0;
	width: 100%;
	height: 100.5%; 
	border: 0px solid transparent;
	background: transparent;
	content: '';
	transition: border-width 0.2s, border-color 0.2s;
}

/* Hover effects */
.grid li.shown:hover h3 {
	color: #fff;
	transform: translate3d(0,-30px,0);
}

.grid li.shown:hover > a::before {
	border-width: 14px;
	border-color: #2E3444;
}

Now, let’s define the animations.

As seen previously, we’ve defined an initial “hidden” state to the curtain element, depending on which animation we’ll be using.

When we scroll the page and an item “moves” into the viewport, we’ll apply the class animate to it and thereby trigger an animation.

For the right swipe effect (the one we see on the Samsung page), we’ll let the curtain element translate to 0, making it move from the left side to the center, and then we’ll translate it out to the right side. By setting the 0 translate in 50% and 60%, we are making sure that the element stays a bit, and does not just swipe from left to right:

/* Swipe right */
.grid.swipe-right li.animate .curtain {
	animation: swipeRight 1.5s cubic-bezier(0.6,0,0.4,1) forwards;
}

@keyframes swipeRight {
	50%, 60% { transform: translate(0); }
	100% { transform: translate3d(100%,0,0); }
}

(Why do we need to use translate(0) here? Well, some browsers, like IE11, seems to have some problem with translate3d(0,0,0) in this context…)

The swipe down effect is almost the same, we just use the Y axis instead of the X axis:

/* Swipe down */
.grid.swipe-down li.animate .curtain {
	animation: swipeDown 1.5s cubic-bezier(0.6,0,0.4,1) forwards;
}

@keyframes swipeDown {
	50%, 60% { transform: translate(0); }
	100% { transform: translate3d(0,-100%,0); }
}

The rotate effect follows the same principle, we just rotate instead of move the element:

/* Swipe rotate */
.grid.swipe-rotate li.animate .curtain {
	animation: swipeRotate 1.5s ease forwards;
}

@keyframes swipeRotate {
	50%, 60% { transform: rotate3d(0,0,1,0deg); }
	100% { transform: rotate3d(0,0,1,-90deg); }
}

The shadow pseudo-element will simply fade out from the moment that the curtain starts moving away, revealing the image:

.grid li.animate .curtain::after {
	animation: fadeOut 1.5s ease forwards;
	animation-delay: inherit;
}

@keyframes fadeOut {
	50%, 60% { opacity: 1; }
	100% { opacity: 0; }
}

Since we will be changing animation delays in our script, we need to make sure that our pseudo-element gets the same value as its parent element. This we can ensure with setting the animation-delay to inherit. While all other elements can have their delay re(defined) easily, it would have difficulties doing something to the pseudo-element with JS.

Last, but not least, we need to hide the image and title initially and only show it when we have reached 60% of our animation. By using the stepping function step-end, which is equivalent to steps(1, end), we can control the visibility and the exact moment, we make the image and the title visible. We need to use the same animation duration like previously so that we can set the visibility to visible at 60%, once the curtain starts moving out again, revealing the now visible image:

.js .grid li img,
.js .grid li h3 {
	visibility: hidden;
}

.grid li.animate img,
.grid li.animate h3 {
	animation: showMe 1.5s step-end forwards;
}

@keyframes showMe {
	from { visibility: hidden; }
	60%, 100% { visibility: visible; }
}

.grid li.shown img,
.grid li.shown h3 {
	visibility: visible;
}

To understand this specific stepping function, check out the brilliant example by Lea Verou: Pure CSS3 typing animation with steps().

Let’s have a look at the JavaScript.

The JavaScript

What we want to do is to show our items when we scroll them into the viewport. Each appearing item will get an animation class that will trigger our previously defined animations. We don’t want the animation to be performed on the first item in our grid, so we’ll simply show them with our show class. We also want to get the dominant color of our images so that we paint the curtain in that color.

So, let’s start with our script options. minDelay and maxDelay define the range for the delay that each animation will have (we pick a random number between these values). This will result in each item animating at slightly different times which makes the effect look much nicer. If we’d want all the animations to start at the same time then we’d just need to set maxDelay to 0. The viewportFactor defines how much of the appearing item has to be visible in order for the animation to start. So 0 (0%) means the animation starts as soon as the item is inside the viewport, and 1 (100%) means the item has to be completely inside the viewport for it to trigger.

GridScrollFx.prototype.options = {
	minDelay : 0,
	maxDelay : 500,
	viewportFactor : 0
}

Let’s initialize and cache some variables and also initialize Masonry. We will need to preload the images in order for the Masonry plugin to work correctly.
Next, we need to distinguish the items that are already in the viewport and the ones that are not, once the page loads. For the ones that are already in the viewport, we will add the class shown to make them visible without any animation. For the items that are not in the viewport we will create the “curtain” element which will be animated once the items are scrolled into view. We will also set the animation delay for all the animations of each item.
Finally we bind the scroll and resize events to the window. We will have a more detailed look at this later on.

GridScrollFx.prototype._init = function() {
	var self = this, items = [];

	[].slice.call( this.el.children ).forEach( function( el, i ) {
		var item = new GridItem( el );
		items.push( item );
	} );

	this.items = items;
	this.itemsCount = this.items.length;
	this.itemsRenderedCount = 0;
	this.didScroll = false;

	imagesLoaded( this.el, function() {
		// show grid
		self.el.style.display = 'block';

		// initialize masonry
		new Masonry( self.el, {
			itemSelector : 'li',
			isFitWidth : true,
			transitionDuration : 0
		} );
		
		// the items already shown...
		self.items.forEach( function( item ) {
			if( inViewport( item.el ) ) {
				++self.itemsRenderedCount;
				classie.add( item.el, 'shown' );
			}
			else {
				item.addCurtain();
				// add random delay
				item.changeAnimationDelay( Math.random() * ( self.options.maxDelay - self.options.minDelay ) + self.options.minDelay );
			}
		} );

		var onScrollFn = function() {
			if( !self.didScroll ) {
				self.didScroll = true;
				setTimeout( function() { self._scrollPage(); }, 200 );
			}
			
			if( self.itemsRenderedCount === self.itemsCount ) {
				window.removeEventListener( 'scroll', onScrollFn, false );
			}
		}

		// animate the items inside the viewport (on scroll)
		window.addEventListener( 'scroll', onScrollFn, false );
		// check if new items are in the viewport after a resize
		window.addEventListener( 'resize', function() { self._resizeHandler(); }, false );
	});
}

Note that we’ve built a different function called GridItem, to hold each item’s data and methods.
When the curtain element is created we set the color of its background. The color will be the most prominent one of the item’s image. It’s retrieved by the Colorfinder plugin:

function GridItem( el ) {
	this.el = el;
	this.anchor = el.querySelector( 'a' ) 
	this.image = el.querySelector( 'img' );
	this.desc = el.querySelector( 'h3' );
}

GridItem.prototype.addCurtain = function() {
	if( !this.image ) return;
	this.curtain = document.createElement( 'div' );
	this.curtain.className = 'curtain';
	var rgb = new ColorFinder( function favorHue(r,g,b) {
		// exclude white
		//if (r>245 && g>245 && b>245) return 0;
		return (Math.abs(r-g)*Math.abs(r-g) + Math.abs(r-b)*Math.abs(r-b) + Math.abs(g-b)*Math.abs(g-b))/65535*50+1;
	} ).getMostProminentColor( this.image );
	if( rgb.r && rgb.g && rgb.b ) {
		this.curtain.style.background = 'rgb('+rgb.r+','+rgb.g+','+rgb.b+')';
	}
	this.anchor.appendChild( this.curtain );
}

GridItem.prototype.changeAnimationDelay = function( time ) {
	if( this.curtain ) {
		this.curtain.style.WebkitAnimationDelay = time + 'ms';
		this.curtain.style.animationDelay = time + 'ms';
	}
	if( this.image ) {
		this.image.style.WebkitAnimationDelay = time + 'ms';
		this.image.style.animationDelay = time + 'ms';
	}
	if( this.desc ) {
		this.desc.style.WebkitAnimationDelay = time + 'ms';
		this.desc.style.animationDelay = time + 'ms';
	}
}

Let’s take a look at what happens when we scroll the page (note that the scroll event function is just being called every 200ms in order to avoid performance issues). First, we will iterate through all our items and check which ones are inside the viewport and are not shown already or currently animating. If an item does not have a curtain element then we will just add the class shown and return, otherwise we will add the class “animate” to trigger our animation. For instance, the first item in our demo grid would be one of those cases. Once the animation ends, we add the class shown and remove the animate class.

GridScrollFx.prototype._scrollPage = function() {
	var self = this;
	this.items.forEach( function( item ) {
		if( !classie.has( item.el, 'shown' ) && !classie.has( item.el, 'animate' ) && inViewport( item.el, self.options.viewportFactor ) ) {
			++self.itemsRenderedCount;

			if( !item.curtain ) {
				classie.add( item.el, 'shown' );
				return;
			};

			classie.add( item.el, 'animate' );
			
			// after animation ends add class shown
			var onEndAnimationFn = function( ev ) {
				if( support.animations ) {
					this.removeEventListener( animEndEventName, onEndAnimationFn );
				}
				classie.remove( item.el, 'animate' );
				classie.add( item.el, 'shown' );
			};

			if( support.animations ) {
				item.curtain.addEventListener( animEndEventName, onEndAnimationFn );
			}
			else {
				onEndAnimationFn();
			}
		}
	});
	this.didScroll = false;
}

As for resizing the window, we need to check if new items are inside the viewport after a resize:

GridScrollFx.prototype._resizeHandler = function() {
	var self = this;
	function delayed() {
		self._scrollPage();
		self.resizeTimeout = null;
	}
	if ( this.resizeTimeout ) {
		clearTimeout( this.resizeTimeout );
	}
	this.resizeTimeout = setTimeout( delayed, 1000 );
}

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

Please note that if you have the experimental Web Platform features enabled in Chrome (on Win), the effect might not be visible (items just appear).

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.