News

Build an animated 3D menu with CSS and JS

Get your menu to stand out with a dynamic 45-degree slide in page view

3dmenu

Get your menu to stand out with a dynamic 45-degree slide in page view

3dmenu

A lot of the projects that are featured in this magazine are done so because they have some unique feature about them. Most of these have some quirky way of interacting with the menu and as the user tends to do most of the interaction with the menu, it’s usually a good place to start when creating a unique focal point for your site.

In this tutorial we are going to look at CSS3 transformations that, by default, are hardware accelerated. As such, we can add lovely 3D effects and transitions to our content by adding and removing classes that will trigger the animations. In this menu we will have an off-screen menu that slides in from the left-hand side, nothing out of the ordinary about that, except that our page content will flip out of the way using a 3D transform enabling the menu to take full focus on the page. Then, when it’s time to bring the page back, the user simply clicks on the page and the menu slides back out and the page rotates back into view. A little trick here to make this work is to stop the page being scrollable while the page is rotated out to one side.


GET THE TUTORIAL CODE


Start the project

Open the start folder in Brackets or place it in your local web server folder. Take a look at the page, before starting the project, in a web browser to see that there is a basic page on display – a menu needs to be added to this. There is a comment in the index.html page showing the end of the twist div, add the menu in here.

001    <nav class="offscreen-nav">
002    <a href="#">Home</a>
003    <a href="#">News</a>
004    <a href="#">Blog</a>            
005    <a href="#">Portfolio</a>
006    <a href="#">Contact</a>
007    <a href="#">About</a>
008    </nav>

Switch to the CSS

Save the index page and move to the style.css in the CSS folder. Add the following code in here. It can go at the bottom of the document, just make sure that it isn’t inside a media query. Here the twist class is being given a relative position on the page. This will hold the menu outside of the page.

001    .twist {
002    position: relative;
003    }

Build the content

All of the real pages go inside the container class. Here it is given a white background because later the menu will be given a red background to match the design on the screen. It’s given a z-index that is higher than the rest of the menu so that all the main content will be visible above this.

001        .container {                
002        background: #fff;
003        min-height: 100%;
004         position: relative;
005         outline: 1px solid rgba(0,0,0,0);
006         z-index: 10;
007        -webkit-transform: translateZ(0) translateX(0) rotateY(0deg);  
008         transform: translateZ(0) translateX(0)rotateY(0deg);
009    

More page positioning

There is a wrapper class just inside the container, again this needs to be set to relative so that when the design opens, it works correctly. When the menu is opened a class gets added to it and this is called ‘open’. Here the twist class is made to be fixed and the perspective added.

001    .wrapper {
002    position: relative;
003    }                                        
004    .twist.open {
005    position: fixed;
006    -webkit-perspective: 1500px;
007    perspective: 1500px;
008    }
009    

Tidy up the open page

When the menu opens, the container that holds the regular page content is made to have no overflow. This helps it to twist out with a 3D perspective to it without having the rest of the page on display. At this point the cursor is set to be a pointer so that the container becomes the button to bring the page back into the main view.

001    .open .container {
002    position: absolute;
003    overflow: hidden;            
004    width: 100%;
005    height: 100%;
006    cursor: pointer;
007    -webkit-backface-visibility: hidden;    
008    backface-visibility: hidden;
009    }  

Add functionality

As the content is being animated in 3D space the open wrapper is given a CSS transformation on the z axis. The container is slightly altered when it is in animation to be slightly bigger than the screen with full opacity. The transition takes less than a third of a second.

001    .open .wrapper {
002    -webkit-transform: translateZ(-1px); 
003    }
004    .animate .container::after {
005    opacity: 1;                        
006    height: 101%;
007    -webkit-transition: opacity 0.3s;
008    transition: opacity 0.3s;
009    } 

Position the navigation menu

Here the navigation for the off-screen menu is set in the CSS. The position is set at absolute so that it can be animated from the side and made to have a height that fits its content. This is given the transform position of 50% of the height and to ensure hardware acceleration the preserve-3d is set.

001    .offscreen-nav {
002    position: absolute;
003    height: auto;
004    font-size: 2em;
005    top: 50%; 
006    -webkit-transform: translateY(-50%);    
007    transform: translateY(-50%);        
008    -webkit-transform-style: preserve-3d;
009    transform-style: preserve-3d;
010    left: 25%;
011    }
012    

Menu items

Each menu element needs to be styled up in the right font weight, with the underline and the margin taken off to make it appear in the right place on the screen. A transition is added so that the text colour can change on rollover and to make it all visible.

001    .offscreen-nav a {
002    display: inline-block;
003    white-space: nowrap;
004    font-weight: 300;
005    text-decoration: none;
006    margin: 0 0 30px 0;
007    color: #fff; 
008    -webkit-transition: color 0.3s;        
009    transition: color 0.3s;            
010    -webkit-transform-style: preserve-3d;
011    transform-style: preserve-3d;
012    }

Finish the menu items

The next CSS will give the hover a bright yellow colour to make it stand out against the red background. The background is set in the effect-persp class to a red colour to match the logo in the page. The white page will rotate out to the right while the menu will animate in from the left on the red background.

001    .offscreen-nav a:hover { 
002    color: #fff72f;
003    }
004    .offscreen-nav a {
005    display: block;            006    }                                        
007    .effect-persp {
008    background: #b40000; 
009    }

Add the transition

In order to make the container swing out and twist in 3D space we need to change its transform origin point to the centre of it. This is given a slightly longer transition time than in Step 6, but it all works together to bring the effect on the screen.

001    .effect-persp .container {
002    -webkit-transition: -webkit-transform 0.4s;
003    transition: transform 0.4s;
004    -webkit-transform-origin: 50% 50%;
005    transform-origin: 50% 50%;
006    }

Close the row

When the container is swinging out, a class of ‘animate’ will be added to it. This is the class that will actually contain the animation. As you can see it rotates the interface on the y axis by 45 degrees. Try and imagine that there is a pin in the top of the screen that rotates the page 45 degrees away from the view.

001    .effect-persp.animate .container {
002    -webkit-transform: translateZ(-1500px)     translateX(100%) rotateY(-45deg);        
003    transform: translateZ(-1500px)         translateX(100%) rotateY(-45deg);        
004    

Second row of columns

The actual links are positioned off the screen to the left so the translateX CSS transform is applied to keep them 150 pixels off the screen to the left. These are animated in from a transparent opacity, in the next step they’ll be given full opacity to be fully visible to the viewer.

001    .effect-persp .offscreen-nav a {
002    opacity: 0;
003    -webkit-transform: translateX(-150px);
004    transform: translateX(-150px);
005    -webkit-transition: -webkit-transform 0.4s, opacity 0.4s;
006    transition: transform 0.4s, opacity 0.4s;
007    }                                        
008    

Final CSS

Finally the menu is brought onto the screen with the full opacity for each menu element. Save the style.css now because it has been completed. There won’t be anything to see in the browser though because there is no functionality added to the page yet. That will come next by applying the CSS classes with JavaScript.

001    .effect-persp.animate .offscreen-nav a {    
002    opacity: 1;
003    -webkit-transform: translateX(0);     
004    transform: translateX(0);
005    }

Start the JavaScript

Open the file twist.js and you will see that it is an empty document ready for us to begin. This tutorial is using the classie.js external library for adding and removing classes with JavaScript to CSS. In this function the code returns how much the page has scrolled.

001    function scrollY() {
002    return window.pageYOffset || docElem.    scrollTop; 
003    }                                        
004    

Setting variables

The next part of the code sets out some variables that are needed in the code. The biggest section is an object containing the browser prefix names. These are used to check when the transition has ended by dynamically adding an event listener to the transition later in the code.

001    var docElem = window.document.documentElement,
002    support = "transition",
003    transEndEventNames = {            
004    'WebkitTransition': 'webkitTransitionEnd',
005    'MozTransition': 'transitionend',        
006    'OTransition': 'oTransitionEnd',
007    'msTransition': 'MSTransitionEnd',
008    'transition': 'transitionend'
009    },
010    transEndEventName = transEndEventNames[ 'transition' ],
011    docscroll = 0; 

Initialise the interface

Most applications have an init function to initialise all the things that are necessary. The first part of this function will get a reference to all the necessary elements in the DOM so that these can be manipulated through the code without having to continuously traverse the DOM.

001    function init() {
002    var showMenu = document.getElementById(     'showMenu' ),
003    twistWrapper = document.getElementById(     'twist' ),                                
004    container = twistWrapper.querySelector(     '.container' ),
005    contentWrapper = container.querySelector(     '.wrapper' );    

Show the menu

Here the menu button is detected for when a user clicks on it. The click event fires the remaining function, which is only partially shown in this step. The event is stopped from propagating and the default action of the button is also prevented. This enables the code to run without default actions interfering.

001    showMenu.addEventListener( 'click',  function( ev ) {
002    ev.stopPropagation();
003    ev.preventDefault();
004    docscroll = scrollY();
005    

Finish the image

The rest of the function is shown here. The scrolling is set to stop at this point as the class of ‘open’ is set to the twistWrapper element. Just marginally after this, triggered by the setTimeout command, another class is added called the ‘animate’ class and this therefore starts the animation.

001    contentWrapper.style.top = docscroll * -1 + 'px';
002    document.body.scrollTop = document.    documentElement.scrollTop = 0;
003    classie.add( twistWrapper, 'open' );    
004    setTimeout( function() { classie.add(     twistWrapper, 'animate' ); }, 25 );
005    });                                    
006    

Return the menu

The container gets an event listener added to it, which also detects input from a click. This should only fire if it has the ‘animate’ class already added to it because that means it’s actually open and needs to go back when clicked, otherwise it won’t fire.

001    container.addEventListener( 'click',     function( ev ) {
002    if( classie.has( twistWrapper, 'animate') ) {
003    var onEndTransFn = function( ev ) {
004    if( support && ( ev.target.className !==     'container' || ev.propertyName.indexOf(     'transform' ) == -1 ) ) return;

Remove the event

If the transitions have finished then the ‘open’ class needs to be removed, which makes sense as the menu isn’t open any more. At this point scrolling is returned to the user so that the page can carry on like a normal web page under the control of the user.

001    this.removeEventListener(         transEndEventName, onEndTransFn );
002    classie.remove( twistWrapper, 'open' );
003    document.body.scrollTop = document.    documentElement.scrollTop = docscroll;
004    contentWrapper.style.top = '0px';
005    };                                        
006    

Removing the animate class

Similar to the previous step the twistWrapper has an event listener added that detects that the transition has finished. When it does it removes the class of ‘animate’ as this is no longer applicable to it. Just the final finishing off of the init function is left.

001    twistWrapper.addEventListener( transEndEventName, onEndTransFn );
002    classie.remove( twistWrapper, 'animate' );
003    }
004    });

Finish off

Add the final code and bracket to close off the init function. The final line calls this function so that the previous code is applied. Now save this JavaScript and open the index.html page in your browser to see the menu open and close while the transition takes full effect.

001    twistWrapper.addEventListener( 'click',     function( ev ) { return false; } );
002    }
003    init();
×