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.
SamsungGrid

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:

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 57

Comments are closed.
  1. Another amazing concept from tympanus, I fall in love the rotate effect.

  2. Just making sure it’s just not merely my particular iPhone (4s): On Safari on the iPhone, the demo crashes. It doesn’t crash on desktop browsers however…

    • Hi Kevin, thanks a lot for your feedback! I tested on the 5 and there the last demo seems to crash, too πŸ™ Does your browser crash in all demos or just the last one? Thanks, ML

    • Mary Lou, I tried it on my iPhone 5 and it seems you are right, the last one crashes. However this is amazing work.

    • The last one is crashing for me too. iOS 7.1.1 on a newer iPad. Clever demo, though! Always great to see neat uses of Masonry.

    • Crashes just on rotate for iPhone 5s safari for me too.

      On chrome for iPhone it crashes immediately, you can’t even see the slide effects.

    • Tried with iphone 5, and if i start out browsing when in landscape mode with safari, the demo works. It crashes as soon as i rotate the phone. same goes for portrait mode to landscape.

  3. This doesn’t seem to be working on the latest Chrome. Works fine on Firefox though.

    • Hi Vince,
      do you have the experimental Web Platform features enabled? That seems to break the effect in Chrome on Windows, it seems… Let me know and thanks! ML

    • Hey Mary Lou,

      Yep, you’re right. Disabling experimental Web Platform features fixed it, thanks.
      And thanks for, yet again, a great cutting-edge UX friendly CSS/JS snippet πŸ™‚

  4. I was wondering how the animation knows what color to load for the background, they seem to match the photos or is that just a coincidence?

  5. The animations look great on desktop. However, I’d speed them up (or even disable them) on mobile. The delay is too big since it’s a 1-column grid and the user only scrolls 1 or 2 grid items into view at a time.

  6. Hi, great tutorial!
    just a question: is it possible to have the animation even for the first elements instead of having them already loaded??
    thank you πŸ˜€

    • GridScrollFx.js Line 159-170 cahnge it with this
      // the items already shown…
      //Activate them all
      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);

      self.didScroll = true;
      setTimeout(function() {
      self._scrollPage();
      }, 200);
      //}
      });

  7. Thanks for awesome tutorial. I totally loved it. But problem is it dose not support browsers like IE8 or IE9. It must have atleast support IE9. I would have glad to use it for my web projects.

    Still great work and I always get loads of inspiration from your work. Keep it coming.

    Samir.

  8. Awesome animation! Hmm a rotate + swipe right effect might look quite interesting! Like, the coloured backgrounds would rotate in, and the individual images swipe right into the frames.

    • Ok I just took a look at Samsung’s original site, and I must say I like your hover effect much more than Samsung’s messy ones. Haha, I think Samsung’s disorganised cover images (which seemingly lack a coherent theme) made it worse.

  9. Fantastic template! Is it possible to end a grid section, insert some other content <div>..., etc., and fire up another grid section? I’m having trouble getting an additional section of boxes to load.

  10. Hi Mary Lou,
    Very nice! Your work is very inspiring as always. I am a big fan of yours!

    I have a challenge for you. If not already done, can you teach us how to reproduce this kind of effect from scratch.
    http(link)://fliphtml5.com/examples/otomobil-news/index.html#p=21
    http(link)://dealfuel.com/seller/flip-html5-mac-create-outstanding-css3-jquery-html5-flipbook-pdf/

    Would love to see your angle on this.
    Thanks
    Gilbert

    • Yes, it’s can apply to wordpress, if you want please contact me and i will help yo.

  11. So I tried following this to the letter to implement in a site, but not working at all – checked your source code and realised I was missing a couple of the scripts (Masonry used to include ImagesLoaded and I think Classie as well by default). Downloaded the same scripts and tested, and now none of the images show on the page. So I downloaded the source code, opened it, seemed fine. Swapped out dummy.png for my own image.png, and none of the images would load.

    Where am I going wrong?
    Thanks,

    Greg

    • hi, I have been having the exact same problem.. if you have found the solution, please let me know as well.. thanks πŸ™‚

    • Hi Greg,
      Did you solve your problem? I seem to have the same problem as you described.. nothing shows up.. thanks,

  12. Wow! really smooth ! amazing work like usually !! πŸ™‚

    Btw, i tried it on my iPhone 4S iOS 7.1.1 with the latest Safari, and latest Chrome (35.0.1916), it crashes. I even cant see the page fully loaded ^^