Creating Windows-8-like 3D animations with CSS3 and jQuery

by Sara Soueidan

I’ve been using Windows 8 for a while now (and loving it), and one of the first things that struck me as impressive about it were the transitions and animations built into the dashboard (which uses the Metro design principles). I thought it would be a really fun experiment to try to duplicate the effects I saw in Windows 8 using HTML, CSS and JavaScript. You can view the live demo of the results or access the full source code via GitHub. In this tutorial I will walk you through exactly how the demo was built.

Note: For the sake of brevity in the example code, I am using the un-prefixed CSS properties, make sure you add them before using them in browsers that need them (you can determine which browsers need prefixes by checking out CanIUse.com).

The Markup

The demo‘s structure is pretty simple: The dashboard is a bunch of “boxes”, of two sizes, big and small, floated inside 3 columns. Each box also has a corresponding “page”. The page is an overlay which opens using a 3D transition when the user clicks on one of the boxes on the dashboard. The page is intended to represent an app on a real desktop, with each of the boxes being the shortcut to that app on the dashboard.

Here is the dashboard structure:

<div class="dashboard clearfix">
  <div class="col1 clearfix">
    <div class="big todos-thumb" data-page="todos">
      <p>My Todos
        <span class="todos-thumb-span">You have 5 more tasks to do!</span>
      </p>
    </div>
    <div class="small lock-thumb">
      <span class="icon-font  center" aria-hidden="true" data-icon=""></span>
    </div>
    <div class="small last cpanel-thumb" data-page="cpanel">
      <span class="icon-font" aria-hidden="true" data-icon=""></span>
    </div>
    <div class="big notes-thumb" data-page="notes">
      <span class="icon-font" aria-hidden="true" data-icon=""></span>
      <p> Notes</p>
    </div>
    <div class="big calculator-thumb" data-page="random-page"></div>
  </div>
  <div class="col2">
    <!--....-->
  </div>
  <div class="col3">      
    <!--....-->
  </div>
</div>

The icon font that is used is from Icomoon. You can see that I’m using span elements for the icons. These can also be added using pseudo elements, but as transitions on pseudo elements aren’t yet supported in all browsers, it is recommended to use span elements instead (for now). You can read more on this topic here.

I’ve also added a  data-page attribute to most boxes that stores the name of the page that will be opened when they are clicked. This will come in handy later in the Javascript code.

For the sake of brevity, only the first two boxes (the Todo App and Lock) have their own pages. You can feel free to add as many boxes and pages as you want, but I wanted the focus of this tutorial to be on the 3D effects instead of the content. The Todo app in the .todos page is also a dummy.

Now let’s look at the structure of the overlay pages:

<div class="page todos">
  <h2 class="page-title">My Todos</h2>
  <!--content of page goes here-->
  <div class="close-button">x</div>
</div>

<div class="page random-page">
  <h2 class="page-title">Some Awesome App!</h2>
  <!--content of page goes here-->
  <div class="close-button">x</div>
</div>

The CSS

For this demo, I’ve created the styles for mobile first. The following code samples contain widths that are applicable to small screens. Towards the end of this section, we’ll take a look at using media queries to adjust them for larger screens.

First, let’s take a look at the the styles for the demo wrapper, which is the container that wraps the entire demo. One important item to note is that, when the wrapper, we need to make sure to set a perspective to activate the 3D space, otherwise, the whole demo will look flat and two dimensional.

/*general styles*/
html{
  height:100%;
  overflow-y:scroll;
  overflow-x:hidden;
}
body{
  width:100%;
  height:100%;
  line-height:1.5;
  font-family:'Lato', sans-serif;
  font-weight:300;
  font-size:16px;
}
ul{
  list-style-type: none;
}

/*dashboard and pages styles*/

.demo-wrapper{
  background:url("1.png");
  background-size:cover;
  padding: 4em .5em;
  width:100%;
  perspective:3300px;
  position:relative;
  overflow:hidden;
  border-bottom:1px solid #eee;
}
.dashboard{
  margin:0 auto;
  width:100%;
  padding:1em;
}
.col1, .col2, .col3{
  width:99%;
  margin:1em auto;
}
.page{
  width:100%;
  height:100%;
  color:white;
  text-align:center;
  font-size:3em;
  font-weight:300;
  position:absolute;
  right:0;
  top:0;
  opacity:0;
  transform-origin: 100% 0%;
  transform:rotateY(-90deg) translateZ(5em);
}
.page-title {
  margin-top:1em;
  font-weight:100;
  font-size:2.5em;
}

/*styling the pages*/

.page.random-page{
  background:#DFD4C1;
}

.page.todos{
  background:#2FB1BE;
}

/*the close button in the upper right corner of each page*/

.close-button{
  font-size:1em;
  width:1em;
  height:1em;
  position:absolute;
  top:1.25em;
  right:1.25em;
  cursor:pointer;
  border:1px solid white;
  line-height:.8em;
  text-align:center;
}

I’ve set the original position of each page in the 3D space by first rotating it about the y-axis (the vertical axis), then I moved the page 5em to the left of the screen by using translateZ. You should always remember that when you transform an element in 3D, you also transform its coordinate system along with it. What I wanted to do is move the page 5em to the left of the screen, but instead of using translateX I needed to use translateZ because after the first transformation (i.e. the rotation about y axis) the coordinate system was also rotated. So now the z-axis points towards the left, and the x-axis is pointing towards you, the viewer.

These are the styles for the dashboard thumbnails, along with the transition defined on :hover.

.big, .small{
  float:left;
  margin:0 auto 1%;
  font-size:2em;
  color:white;
  text-align:center;
  height:4.5em;
  font-weight:300;
  overflow:hidden;
  padding:.75em 1em;
  cursor:pointer;
  transition:all 0.3s ease-out;
}
.big:hover, .small:hover{
  background:white;
}
.big{
  width:100%;
}
.small{
  width:49%;
  margin-right:2%;
}
.big p {
  line-height:1.5;
  margin-top:.6em;
  padding:0 .3em;
  transition:all 0.3s ease-out;
}
.small.last{
  margin-right:0;
}

/*icon fonts styles*/

.icon-font{
  font-size:2em;
}
.big .icon-font{
  float:left;
}
.lock-thumb .icon-font{
  margin-left:25%;
}

/*styling the dashboard boxes*/

.weather-thumb {background:#F2854C;}
.weather-thumb:hover {color:#F2854C;}

.paint-thumb {background:#85A9C3;}
.paint-thumb:hover {color:#85A9C3;}

.cpanel-thumb {background:#83A8C3;}
.cpanel-thumb:hover {color:#83A8C3;}

.games-thumb {background:#04ACAD;}
.games-thumb:hover {color:#04ACAD;}

.news-thumb, .calculator-thumb {background:#EBB741;}
.news-thumb:hover, .calculator-thumb:hover {color:#EBB741;}

.videos-thumb, .shortcut-thumb{background:#BEA881;}
.videos-thumb:hover, .shortcut-thumb:hover{color:#BEA881;}

.lock-thumb, .alarm-thumb {background:#EF3A5B;}
.lock-thumb:hover, .alarm-thumb:hover {color:#EF3A5B;}

.piano-thumb, .favorites-thumb, .notes-thumb {background:#385E82;}
.piano-thumb:hover, .favorites-thumb:hover, .notes-thumb:hover {color:#385E82;}

.photos-thumb {background:#BEA881;}
.photos-thumb:hover {color:#BEA881;}

.calendar-thumb, .organizer-thumb {background:#8BBA30;}
.calendar-thumb:hover, .organizer-thumb:hover {color:#8BBA30;}

.todos-thumb {background:#2FB1BE;}
.todos-thumb:hover {color:#2FB1BE;}

.todos-thumb p{
  margin-top:.8em;
}
.todos-thumb-span{
  display:block;
  margin-top:1.5em;
}
.todos-thumb:hover p{
  margin-top:-2.7em;
}

Each thumbnail contains an icon and may or may not also contain a paragraph with the text content. The box’s overflow is set to hidden in order to hide the span that reaches below the bottom border of the box. The paragraph has a margin-top set to .8em and, once the thumbnail is hovered, the margin is set to -2.7em, thus transitioning the thumbnail title and the details span upwards to the center of the thumbnail.

All the pages, except the login screen, have the same initial position in the 3D space. Once a box is clicked, a corresponding class is added to the page that will open via JavaScript. Each class calls for a specific animation meaning that each page will get a class name that will define the 3D transition it will use.

These are the available class names:

.openpage{
  animation: rotatePageInFromRight 1s cubic-bezier(.66,.04,.36,1.03) 1 normal forwards;
}
.slidePageLeft{
  transform: rotateY(0) translateZ(0) ; opacity:1;
  animation:slidePageLeft .8s ease-out 1 normal forwards;
}

I’m using the animation shorthand property here. The last value, “forwards,” corresponds to the animation-fill-mode property. This must be set to “forwards,” otherwise the page will go back to its initial “closed” position after the animation is over. In order to keep the page open, and be able to create sequential animations, the element has to stay in the final state defined by the first animation, and from there we can start the second animation.

These are the animations for the pages:

@keyframes rotatePageInFromRight{
  0% {transform:rotateY(-90deg) translateZ(5em);opacity:0}
  30% {opacity:1}
  100% {transform: rotateY(0deg) translateZ(0) ; opacity:1}
}

/*When the close-button is clicked, the page slides to the left*/
/*note that the start of the second animation is the same state as the
end of the previous one*/

@keyframes slidePageLeft{
  0% {left:0; transform: rotateY(0deg) translateZ(0) ; opacity:1}
  70% {opacity:1;}
  100% {opacity:0; left:-150%; transform: rotateY(0deg)}
}

In order for the second animation to start off from where the first one finished, you have to define the final state of the first animation in the initial state of the second one.

The login screen (which could just as well be any content screen, but I chose to make it look like a login screen), is initially positioned -150% to the left, which is off canvas to the left of the screen. When the lock thumbnail is clicked, the screen gets a class name that will trigger the animation and so it slides into view. The input on the login screen is a dummy, so no password is required. Simply press the unlock button and the screen will slide back to the left while also appearing to be “sliding backwards” along the way.

Here are the styles of the login screen, the class name applied to the screen when its thumb is clicked:

.login-screen{
  background:#EF3A5B;
  height:100%;
  width:100%;
  position:absolute;
  top:0;
  left:-150%;
  color:white;
  text-align:center;
  font-weight:300;
  z-index:1;
}
  .login-screen p{
    font-size:6em;
    margin-top:2em;
    font-weight:100;
  }
  .myform{
    margin:2em auto;
    width:300px;
  }

    input{
      display:block;
      line-height:40px;
      padding:0 10px;
      width:260px;
      height:40px;
      float:left;
    }
    #unlock-button{
      background:black;
      color:white;
      font-size:1em;
      float:left;
      border:0;
      height:2.5em;
      width:2.5em;
      padding:.3125em;
      text-align:center;
      cursor:pointer;
      border-radius:2px;
    }

And here are the the animations triggered upon locking/unlocking the login screen:

.slidePageInFromLeft{
  animation: slidePageInFromLeft .8s cubic-bezier(.01,1,.22,.99) 1 0.25s normal forwards;
}
.slidePageBackLeft{
  opacity:1;
  left:0;
  animation: slidePageBackLeft .8s ease-out 1 normal forwards;
}
@keyframes slidePageInFromLeft{
  0% {opacity:0; }
  30% {opacity:1}
  100% {opacity:1; left:0;}
}
@keyframes slidePageBackLeft{
  0% {opacity:1; left:0; transform: scale(0.95);}
  10% {transform: scale(0.9);}
  70% {opacity:1;}
  100% {opacity:0; left:-150%;}
}

Now let’s get to the dashboard animation.

The dashboard also fades into view and fades back when a thumbnail is clicked. Once a thumb is clicked, the dashboard translates back along the z-axis, decreases in size, and fades its opacity gradually until it becomes 0. When a page is closed, the dashboard fades back into view.

The three columns in the dashboard fade in one after the other, with a slight delay between them. When a page is closed, a class name is added to each column (again via JavaScript), and each of these classes calls the animation with a specific delay.

Here are the classes and the animations applied to the dashboard:

.fadeOutback{
 animation: fadeOutBack 0.3s ease-out 1 normal forwards;
}
.fadeInForward-1, .fadeInForward-2, .fadeInForward-3 {
  /*remember: in the second animation u have to set the final values reached by the first one*/
  opacity:0;
  transform: translateZ(-5em) scale(0.75);
  animation: fadeInForward .5s cubic-bezier(.03,.93,.43,.77) .4s normal forwards;
}

.fadeInForward-2{
  animation-delay: .55s;
}
.fadeInForward-3{
  animation-delay: .7s;
}

@keyframes fadeOutBack{
  0% {transform: translateX(-2em) scale(1); opacity:1;}
  70% {transform: translateZ(-5em) scale(0.6); opacity:0.5;}
  95% {transform: translateZ(-5em) scale(0.6); opacity:0.5;}
  100% {transform: translateZ(-5em) scale(0); opacity:0;}
}
@keyframes fadeInForward{
  0% {transform: translateZ(-5em) scale(0); opacity:0;}
  100% {transform: translateZ(0) scale(1); opacity:1;}
}

Now let’s define how the styles will adjust to various widths using media queries. The three columns of the dashboard will change from having full width and will be floated beside each other.

@media screen and (min-width: 43.75em){
  .col1,.col2,.col3{
    float:left;
    margin-right:1%;
    width:49%;
  }
}
@media screen and (min-width: 64em){
  .col1,.col2,.col3{
    float:left;
    margin-right:.5%;
    width:32%;
  }
  .col3{margin-right: 0;}
  .col1{margin-left:2em;}

}

The Javascript

All click events in the demo app are handled with JavaScript. For this example, I am using jQuery. Event handlers are going to be set on each of the dashboard boxes, and when a click event is detected, we’re going to retrieve the name of the corresponding page from the data-page attribute, and open that page.

Other click events that will be handled include clicking on the close button in each page or the unlock button in the login screen.

function showDashBoard(){
  for(var i = 1; i <= 3; i++) {
    $('.col'+i).each(function(){
        $(this).addClass('fadeInForward-'+i).removeClass('fadeOutback');
    });
  }
}

function fadeDashBoard(){
  for(var i = 1; i <= 3; i++) {
    $('.col'+i).addClass('fadeOutback').removeClass('fadeInForward-'+i);
  }
}

$('.big, .small').each(function(){
  var $this= $(this),
      page = $this.data('page');
  $this.on('click',function(){
    $('.page.'+page).addClass('openpage');
    fadeDashBoard();
  })
});

$(".lock-thumb").click(function(){
    fadeDashBoard();
    $('.login-screen').addClass('slidePageInFromLeft').removeClass('slidePageBackLeft');
});

$('#unlock-button').click(function(){
      $('.login-screen').removeClass('slidePageInFromLeft').addClass('slidePageBackLeft');
      showDashBoard();
});

$('.close-button').click(function(){
  $(this).parent().addClass('slidePageLeft')
  //this function will detect the end of the animation, and remove the classes added before
  //so that the page will get back to its initial position after it has been closed
        .one('webkitAnimationEnd oanimationend msAnimationEnd animationend', function(e) {
              $(this).removeClass('slidePageLeft').removeClass('openpage');
            });
    showDashBoard();
});

Where to go from here

As discussed earlier, you can feel free to fork the GitHub repository for this demo application and modify or use the code as you wish. I hope you enjoyed this tutorial, and I’d love to hear your comments.

Source: This article was originally published at https://blog.sarasoueidan.com/windows8-animations/index.php

Previous

How to create an advanced HTML5 placeholder polyfill

Create Your First Mobile App with PhoneGap Build – Using the Storage API

Next