3D Grid Effect

A proof-of-concept effect recreation of the animation seen in a prototype app by Marcus Eckert. The idea is to flip a grid item in 3D, expand it to fullscreen and reveal some associated content. We've created two demos with a vertical and a horizontal rotation.


View demo Download source

Today we’d like to share a little animation concept with you. It’s the recreation of an effect we spotted in this fantastic prototype app by Marcus Eckert. The idea is to rotate a grid item in 3D, expand it into fullscreen and reveal some content. For our attempt to imitate the app behavior, we created two demos. In the first one we rotate the grid item vertically and in the second one horizontally.

Please note that this is just a proof-of-concept and that we are using several CSS properties that might not work in every browser (pointer-events, 3D Transforms, CSS Transitions). It is highly experimental and for browsers not supporting either one of those properties, we provide a simple fallback that just shows and hides the content.

The amazing illustrations featured in the demos are by Adam Quest. View all images from the Selected editorials 2014 Vol.1 on Behance and visit his Facebook page.

The way we simulate the effect seen in the prototype app video, is by creating a placeholder element that will have a front and a back face. The front will contain a copy of the grid item content and the back will be white. When clicking on a grid item, we add the absolutely positioned placeholder and position it in the clicked item’s position. Then we animate its width and height to the window’s width and height and set the top and left to negative values, making it move outside of the grid into the top left corner of the page. While we are doing that, we’ll also rotate the element in 3D, revealing the white back side of the placeholder. Once it fills the screen, we’ll fade in the content which we stored in a different division.

The illusion, that it’s actually the grid item that flies out and rotates, is completed by fading out the clicked grid item so that we can only see the placeholder being moved out of the grid.

In addition to the placeholder moving towards the viewer, we also move the grid away by translating it on the Z-axis.

Please note, that this is just a proof-of-concept. You might want to load your content dynamically (see the dummy loading effect) or show something else as the main content, i.e. a fullscreen image. The fallback is also very simple and general.

Let’s take a look at the markup and some important styles to understand the concept of how the effect is done.

We have a main section element which contains a division for the grid and one for the content:

<section class="grid3d vertical" id="grid3d">
	<div class="grid-wrap">
		<div class="grid">
			<figure><img src="img/1.jpg" alt="img01"/></figure>
			<figure><img src="img/2.jpg" alt="img02"/></figure>
			<figure><img src="img/3.jpg" alt="img03"/></figure>
			<!-- ... -->
	</div><!-- /grid-wrap -->
	<div class="content">
			<div class="dummy-img"></div>
			<p class="dummy-text">Some text</p>
			<p class="dummy-text">Some more text</p>
			<!-- ... -->
		<!-- ... -->
		<span class="loading"></span>
		<span class="icon close-content"></span>

When we click on a grid item, we’ll go by order to choose the matching content division. The content division also contains two spans, one for the activity indicator to simulate some loading, and one for the closing cross.

The placeholder for our rotation and expansion effect will be added dynamically to the grid and the structure is the following:

<div class="placeholder">
	<div class="front"><!-- content of clicked grid item --></div>
	<div class="back"></div>

Let’s have a look at some crucial styles.

The grid-wrap division will be the element with perspective:

.grid-wrap {
	margin: 10px auto 0;
	max-width: 1090px;
	width: 100%;
	padding: 0;
	perspective: 1500px;

In addition to the perspective value, we also set the perspective origin. But that we do dynamically depending on the view. If you, for example, view this grid on a smaller device where the grid height becomes very large, we don’t want the default perspective origin of 50% 50% because then an item that is far from the center of the whole grid will rotate away from the viewport (down or up). We’ll set the origin to be central to the viewer instead.

The grid will have a transition and we need to assign a preserve-3d transform-style to it because we want to be able to rotate its children in 3D:

.grid {
	position: relative;
	transition: all 0.5s cubic-bezier(0,0,0.25,1);
	transform-style: preserve-3d;

When we click on a grid item, we add the class view-full to the grid which will make the grid move away from us:

.view-full .grid {
	transform: translateZ(-1500px);

When we click on a grid item, we’ll make it fade out (our placeholder will be put in place):

.grid figure.active {
	opacity: 0;

The placeholder will have its pointer-events set to none and it will be positioned absolutely, unlike the other figures in the grid. It also needs the preserve-3d transform-style because we want to flip its backside:

.grid .placeholder {
	pointer-events: none;
	position: absolute;
	transform-style: preserve-3d;
	transition: all 0.5s ease-out;

The front and back side of the placeholder follow the classic 3D “card” style:

.placeholder > div {
	display: block;
	position: absolute;
	width: 100%;
	height: 100%;
	backface-visibility: hidden;

.placeholder .front img {
	width: 100%;
	height: 100%;

.placeholder .back {
	background: white;
	transform: rotateY(180deg);

The cloned image that we add to the front face of the placeholder is stretched to the full width and height to fill all the element.

Once we open a grid item we want the transition to have a bit of a delay and a custom timing-function:

.view-full .placeholder {
	transition: all 0.5s 0.1s cubic-bezier(0,0,0.25,1);

Depending on which kind of rotation we want, we set the according class to the main wrapper and set the following transform:

.vertical .view-full .placeholder {
	transform: translateZ(1500px) rotateX(-179.9deg);

.horizontal .view-full .placeholder {
	transform: translateZ(1500px) rotateY(-179.9deg);

This should of course be -180 degrees! But because browsers seem to have the liberty to choose which direction they rotate in this case, we need to set it to this awkward number (Firefox and Chrome choose opposing directions with -180 degrees).

The content division has fixed positioning and it will be fullscreen. We also want its inner content divisions to be scrollable, so we set the overflow-y to scroll and add -webkit-overflow-scrolling: touch for better mobile scrolling behavior:

.content {
	/* ... */
	overflow-y: scroll;
	height: 0;
	background: #fff;
	visibility: hidden;
	z-index: 400;
	-webkit-overflow-scrolling: touch;

When we want to open a specific content division, we first show the main content container:

.content.show {
	height: auto;
	pointer-events: auto;
	visibility: visible;

Each content division is placed absolutely and we set the initial opacity value to 0. We also need to set the height to 0 so that other invisible divisions don’t define the height of the whole thing.
To show a specific division, we need to set the following style:

.content > div.show {
	height: auto;
	opacity: 1;	
	transition: opacity 0.6s;

For the dummy content we define some transitions for the opacity and the transform. Depending on which rotation we choose, we set the initial translation of the dummy items to translate on the Y or the X-axis. Then, when we add the class “show” to their parent, we animate the translation to 0.

Take a look at the commented JavaScript to see how the functionality is implemented. As mentioned before, the fallback for browsers that don’t support some of the CSS properties in use, we provide a very basic fallback.

We hope you enjoy this little effect and find it inspiring!

View demo Download source


Tagged with:

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.

View all contributions by

Website: http://www.codrops.com

Related Articles

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 leanirng CSS now.

Feedback 54

Comments are closed.
  1. 1

    Love it, only it’s not depricating well on mobile devices (testing on Android; didn’t go past that one since it didn’t work there): Can’t close the destination screen; actually, you can, however you need to scroll to the end first, and even then it’s glitchy. But definitely something to keep in mind as technology catches up to your creativity! Would love to use it if anyone has a more full proof fallback to suggest.

  2. 2

    Hi Mary Lou! Appreciate your work !
    Just simple question:
    How can I change an images after I click on the tile ( dummy_img) that they gonna be different for each tile?

  3. 3

    How can i edit the display text, when user click on the pic ?
    The text shown is dummy and can’t able to identify, from where it would be edited .

    Any help ll be highly appreciated.


  4. 4

    Any knows how to remove the ‘All’ from the filter. I don’t want it there and currently trying to remove it from the elastic_grid.js and also the within the HTMl script but no joy.
    Any suggestions would be appreciated. Thanks in advance

  5. 5

    Hi Mary Lou,

    Great tutorial. I have tried testing it out on a test page but it’s not working. When I checked my chrome console it says,

    Uncaught TypeError: Cannot read property ‘children’ of null
    grid3D._init @grid3d.js:37
    grid3D @grid3d.js:19
    (anonymous function) @test.html:298 – This line is from my test file that has the script “new grid3D( document.getElementById( ‘grid3d’ ) );”

    Every scripts and code is include and triple checked. I didn’t make any changes. I didn’t include all the images, just 2 for simple testing but it’s just not working. I looked at your code in the console but there was no error like mine.

    Do you know what am doing wrong? Your help would be greatly appreciated

  6. 6

    Thanks for nice script with demo

    I have one question.
    Effect not working after the ajax resposive, as per my knowlege we need to reinislize javascript,

    also i added this code “new grid3D( document.getElementById( ‘grid3d’ ) );” at ajax response but not working..
    any one have idea then let me know.
    Yuvraj R K

  7. 9

    Hello Mary,

    Wonder full script i have ever used , but there is problem with multiple figure images, i am using a dynamic grid of more than 25 images, so that classie.js is given problem “Uncaught TypeError: Cannot read property ‘classList’ of undefined” , i ahve search a lot but no solution for that, please help me !!!
    Please reply as soon as possible.

Comments are closed.