in Code & Design

Building a Smooth Sliding Mobile Menu

It’s possible to create user interface animations in the browser that are as buttery smooth as native app animations. There are a few important techniques that you’ll need to know in order to achieve that level of performance.

This site is built to be responsive, meaning when you size down your browser window or visit on a mobile device the design adjusts to fit.

The biggest change is the top navigation menu, when the width of the viewport becomes narrow enough it will collapse into what is now commonly called a “burger button”. I guess it resembles a burger from the side? In any case, tapping that button will slide in a menu specifically designed for mobile devices. Give it a try, I’ll wait.

You might notice that the performance of the menu transition is smooth and silky, and the burger button responds instantly to your finger tap. On modern devices it’s almost indistinguishable from the performance you’d expect in native apps.

In this post I’m going to run through all the implementation techniques you’ll need to know in order to get that level of animation performance on your own websites or web apps. Let’s get into it!

1. Build out the HTML

The first step is to build out the HTML that we’ll need for the page and the menu. You’ll need two containers, one for everything on your page, and one for the menu. I’m using UL and DIV elements here, but you can use whatever makes the most semantic sense on your page. I’ve also added a link that will eventually act as the way to toggle the menu open and closed.

<ul>
 <li><a title="Home" href="/">Home</a></li>
 <li><a title="About Me" href="/about">About Me</a></li>
 <li><a title="Contact Me" href="/contact">Contact Me</a></li>
</ul>
<div id="page">
<header>
<h1>My New Site</h1>
<a id="toggle-menu" href="#">Menu</a></header></div>

The order of the menu and the page elements doesn’t matter, although semantically the menu makes the most sense at the top of your page.

2. Style the Menu with CSS

We’ll need some basic styles in order for the menu to appear on the top right hand side of the screen and remain a specific width regardless of the screen size. Feel free to adjust these styles so the menu looks exactly how you’d like.

I’ve chosen to give the menu a width of 240px for a couple of reasons, it feels like a good width for a vertical menu, and it will fit across a wide range of devices while still leaving part of the page visible. Add the following CSS to your page:

html, body { 
 margin: 0; padding: 0; height: 100%; /* For the demo since the page is mostly empty */ 
 background: #333; /* Same color as your menu */ 
} 

#page { 
 position: relative; /* Set the position property so z-index will apply */ 
 z-index: 20; /* Make sure this is higher than #menu */ 
 padding: 20px; 
 background: #fff; 
 height: 100%; 
} 

#page h1 { 
 margin: 0; 
} 

#toggle-menu { 
 position: absolute; 
 top: 10px; 
 right: 10px; 
} 

#menu { 
 display: none; 
 position: absolute; 
 top: 0; 
 right: 0; 
 width: 190px; 
 padding: 15px 25px; 
 margin: 0; 
 list-style: none; 
 background: #333; 
 z-index: 10; /* Make sure the z-index is lower than the #page */
} 

#menu a { 
 display: block; 
 color: #fff; 
 padding: 15px 0; 
 border-bottom: 1px solid rgba( 255, 255, 255, 0.05 );
} 

While you are styling the menu you may want to remove the display: none and reverse the z-index settings so it will sit on top of the page. Once you’re happy with the design make sure you set them back so the page covers the menu entirely and it’s hidden until requested.

3. Avoiding the Browser Reflow

Now, at this point you might be thinking we can sprinkle some jQuery .slideIn() and .slideOut() magic on the menu and be done with it. Although that would work reasonably well on desktop browsers, it will perform poorly on mobile devices.

Using jQuery’s built in animation functions will animate the menu in a way that causes something called browser reflow. This is where the browser has to re-calculate and render the position of elements on your page each time something changes. If you’re animating your menu that’s a lot of frames and a lot of re-calculation, causing the animation to be sluggish on less powerful mobile devices.

There’s a way we can animate elements in the DOM and avoid causing reflow, it’s using a CSS property called transform.

4. CSS Transform and the GPU

Using the transform property will allow us to manipulate any element over three dimensions using the translate3d value. We can move the element on the X, Y and Z axis on the page. So for example to move the #page element 10px to the left, we could use:

#page {
 transform: translate3d( -10px, 0, 0 ); /* X, Y, Z */ 
 -webkit-transform: translate3d( -10px, 0, 0 );
} 

Notice that we use a negative value to shift the #page element to the left. You need to think of the elements’ starting position always as 0, 0, 0 regardless of any other positioning applied to the element in your CSS.

It’s also important to include the vendor prefixed -webkit-transform version in your CSS for the best level of support across multiple browsers and versions. This will ensure your menu works on all modern desktop and mobile browsers to at least a few versions back.

CSS also provides the more axis specific transform options translateX() and translateY(), so why are we using translate3d()? The reason is we want to use hardware acceleration to eventually animate the transform. Using translate3d() allows us to access the power of the GPU (Graphics Processing Unit) on the mobile device to render the element and eventual animation. The GPU is designed for this sort of work so it can provide smoother animation than using the CPU alone.

When we apply translate3d() to an element, a GPU rendered copy of the element is created and placed on a separate layer from the rest of the page. This means any manipulation of that element will not cause browser reflow — we are only modifying that layer and not affecting any other elements on the page below. This dramatically improves the performance of animation on mobile devices.

So how can we make use of the CSS transform property to animate our menu? First of all we’ll need to track the state of the menu using JavaScript.

5. Add JavaScript to Track State

Using JavaScript to track the state of the menu will allow us to add CSS classes to the body element of the document. This will allow us to handle all of the animation purely in CSS.

If you take another look at the menu animation on this site you’ll notice that the menu is not actually moving. What is happening is the menu stays in one place — stuck to the top right edge — and the whole page on the layer above slides back to reveal it.

There are four distinct states:

  1. No animation, menu not visible
  2. Page animating to the left, revealing the menu
  3. No animation, menu is visible
  4. Page animating to the right, hiding the menu

We need to assign CSS classes to the body element to represent when we are in one of these four states. To do this you’ll need to add the following JavaScript to your footer (make sure you have jQuery loaded before this, or you can adapt it to raw JavaScript):

( function( $ ) {  
/**** 
 * Run this code when the #toggle-menu link has been tapped or clicked 
 */  
 $( '#toggle-menu' ).on( 'touchstart click', function(e) {   
  e.preventDefault();   
  var $body = $( 'body' ), $page = $( '#page' ), $menu = $( '#menu' ),       

  /* Cross browser support for CSS "transition end" event */       
  transitionEnd = 'transitionend webkitTransitionEnd otransitionend MSTransitionEnd';   

  /* When the toggle menu link is clicked, animation starts */   
  $body.addClass( 'animating' );   
  
  /***
   * Determine the direction of the animation and
   * add the correct direction class depending
   * on whether the menu was already visible.
   */
   if ( $body.hasClass( 'menu-visible' ) ) {
    $body.addClass( 'right' );
   } else {
    $body.addClass( 'left' );
   }

   /***
    * When the animation (technically a CSS transition) 
    * has finished, remove all animating classes and
    * either add or remove the "menu-visible" class
    * depending whether it was visible or not previously.
    */
    $page.on( transitionEnd, function() {
     $body.removeClass( 'animating left right' ).toggleClass( 'menu-visible' );    
     $page.off( transitionEnd );
    } );
  } );
} )( jQuery ); 

Now, with this JavaScript you’ll see the following body classes appear and disappear depending on the menu state:

  1. No animation, menu not visible — No classes on body
  2. Page animating to the left, revealing the menu — animating & left classes on body
  3. No animation, menu is visible — menu-visible class on body
  4. Page animating to the right, hiding the menu — animating & right classes on body

We’ll see why it’s important to know about these four different states and have distinct classes for each in the next step.

6. Let’s Animate!

From here on our all of the animation will be done using CSS transitions. We can now use the CSS classes that the JavaScript added to the body element to determine when to apply CSS transition properties to elements. Add the following CSS to your page right below the previous styles:

/* Show the menu when animating or visible */
.animating #menu, .menu-visible #menu {
  display: block;
}

/***
 * If the animating class is present then apply
 * the CSS transition to #page over 250ms.
 */
.animating #page {
  transition: transform .25s ease-in-out;
  -webkit-transition: -webkit-transform .25s ease-in-out;
}

/***
 * If the left class is present then transform
 * the #page element 240px to the left.
 */	
.animating.left #page {
  transform: translate3d( -240px, 0, 0 );
  -webkit-transform: translate3d( -240px, 0, 0 );
}

/***
 * If the right class is present then transform
 * the #page element 240px to the right.
 */
.animating.right #page {
  transform: translate3d( 240px, 0, 0 );
  -webkit-transform: translate3d( 240px, 0, 0 );
}

/***
 * If the menu-visible class is present then
 * shift the #page 240px from the right edge
 * via position: absolute to keep it in the 
 * open position. When .animating, .left and
 * .right classes are not present the CSS
 * transform does not apply to #page.
 */
.menu-visible #page {
  right: 240px;
}

I think the first four declarations should hopefully make sense. The first is easy, the #menu element should not be hidden when animating or visible. The second makes sure that the #page element will have a transition applied to it when transformed. That basically means it will animate smoothly over .25 seconds to the transformed position rather than just jumping directly to it.

The next two declarations determine the direction and final position that the #page element should be transformed to. So if it’s .left then it goes 240px to the left, if it’s .right, then 240px to the right from its initial spot (remember it always starts at 0, 0, 0).

The final declaration is a little tougher. When the .animate, .left and .right classes are not present, which is when the menu is either fully visible, or fully hidden, none of the CSS transforms above will apply. That means that there’s no translate3d being set on #page. As soon as the animation classes are removed, and the .menu-visible class is added, #page is going to jump back to its original position at position: absolute; top: 0; right: 0. In order to stop this we need to adjust its position so it does not jump back after opening. We need to adjust it to right: 240px; to keep it open at 240px from the right edge of the page.

You might be thinking — why not just leave the .animating and .left classes on the body to keep the #page in the correct position? It’s true that this would retain the final position of the #page element after the transition. The issue is that it also leaves the element in a state of being rendered by the GPU since the translate3d() value still applies. This is a bad idea, ending up with too many elements being GPU rendered at once will cause mobile browsers to crash.

As a best practice you should only apply translate3d() when absolutely necessary during animation states. Use standard positioning to retain an elements’ final state after animating. This is the main reason we needed to track and apply CSS classes for the four distinct states.

This last CSS declaration will eliminate any flickering of elements while they are in a state of being animated, it only applies to webkit based browsers (most current mobile browsers). Add it below the CSS already added:

#page, #menu {
 -webkit-backface-visibility: hidden;
 -webkit-perspective: 1000;
} 

7. Make it Responsive (if needed)

You should now have a basic page with a sliding menu. With the code above the menu will always be present and working on any modern device, at any screen size. For it to only show on smaller mobile devices you’ll need to introduce a media query to your CSS. Here’s one that will apply to most tablets and smartphones:

/* Hide the #toggle-menu element on larger screens */ 
#toggle-menu {
 display: none;
} 

@media only screen and (max-width: 768px) {
 #toggle-menu {
  display: block;
 }
 
 /* Place all the CSS for your mobile #menu here */
} 

You may want to be more granular with your media queries depending on the size and type of devices you want to support. The above is a fairly blunt approach and will keep the #menu and #toggle-menu elements hidden on bigger screens entirely. You may want to write some alternative styles for #menu on larger screens, or include an entirely separate menu inside of #page for larger screens that will be hidden on mobile devices.

That’s it! You should now have a smooth native-like slide in menu. Here’s a demo of the final version. There are many other great uses for CSS transforms, transitions, and animations, some of which I’ve covered in recent posts: JavaScript Pull to Refresh for the Web, and JavaScript Swipe Cards UX. With the right techniques it’s possible to replicate many of the buttery smooth animations you’d generally expect only in native apps.

I’ve packed up all of the code and put it on Github. You can use it as a base to adapt to your needs. I look forward to seeing some of your implementations, let me know if you can think of any good improvements to the code in the comments.

View on GitHub | Try a Demo