How to Create Windows-8-like 3D animations with CSS3 and jQuery
Browser Support:
(latest versions)
For the sake of brevity in the example code, I am using the un-prefixed CSS properties, but you will find the prefixes in the downloadable source code.
I have recently realized that it's been such a long time since CSS3 animations were introduced into the web, and yet I haven't really experimented or made any demos with them before.
I've also been using Windows 8 for a while now (and loving it), and the first thing I thought was impressive about it, was the transitions and animations built into the dashboard, so I thought it would be really cool if my first experiment with 3D would be to duplicate the effects I saw in windows 8 into a demo of my own.
So, here goes the tutorial on how I did that.
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.
And then there are their corresponding "pages". A page is an overlay which opens when u click on one of the boxes on the dashboard. It represents an app on a real desktop, with each of the boxes being the shortcut to that app.
So, when a box is clicked, its corresponding page opens up in a neat 3D effect.
So 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 I'm using is from Icomoon. You can see that I'm using span elements for the icons, which 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). More on this here.
I've also added a random-page data attribute to most boxes as the page that will be opened when they are clicked, for the sake of brevity, only the first two boxes (Todo App and Lock) have their own pages. You can add as many boxes and pages as you want, I wanted the focus of this tutorial to be on the 3D effects instead of the content.
The to-do app in the .todos page is also dummy, but I plan on making another tutorial soon on how to make a functional to-do list, so stay tuned!
Note that I'm using data attributes on each box element to store the name of the corresponding page that will open when the box is clicked. This will come in handy later in the Javascript code, as it will help save many lines of code.
Now the structure of the overlay pages:
<div class="page todos">
<h2 class="page-title">My Todos</h2>
<div class="close-button">x</div>
</div>
<div class="page random-page">
<h2 class="page-title">Some Awesome App!</h2>
<div class="close-button">x</div>
</div>
The CSS
Please take note that I'm doing the styles mobile-first.. i.e the following widths are applicable to small screens, and will change to big screen styles in the media queries at the end of this section.
First, the styles for the demo wrapper, the container in which the whole demo will be contained.
We style the wrapper, and 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. Always remember: when u transform an element in 3D, you 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 used translateZ, because after the first tranformation (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 with or without a paragraph with the text content. The box's overflow is set to hidden, to hide the span that reaches below the bottom border of the box. The parapraph 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 upwards 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 the thumbnail for each page is clicked, a corresponding class is added (via javascript) to the page that will open, and each class calls for a certain animation. So, each page will get a class name that will define the 3D effect for the page.
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, which must be set to forwards, otherwise the page will get back to its initial "closed" position after the animation is over. So, 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 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 might as well be any-content screen, I just chose to make it a log in screen), is initially positioned -150% to the left, i.e. 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 out. The input is dummy, no password is required, you can 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, and the animations triggered upon locking/unlocking the screen:
.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;
}
.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 the 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 till it becomes 0. And when an opened page is closed, the dashboard fades back into the 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 (via Javascript), and each of these classes calls the animation with a certain 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;}
}
And that's all for the styles and animations.
Now let's define the style changes with media queries:
The three columns of the dashboard will change from having full width, and will be floated beside eachother.
@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 will be handled wtih Javascript. I'll be using jQuery in this code. 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 use it to open that page.
Other click events will be handled when clicking on the close button in each page, and 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();
});
And that's it. :)
I hope you enjoyed this tutorial, and if u did make sure you share it!