Scrollable off-canvas navigation in HTML mobile app

There are many different ways to achieve nice and fluid off-canvas navigation in mobile websites. Some simple solutions can be easily implemented in you mobile app without too much of a trouble. The problem comes when we want to create menu similar to those in native apps, like Google+ or new Gmail app. We start to design it on the computers, we put some JS and CSS definitions – everything works fine! By the time we open our app on the phone we realize that something is not quite right… and it get’s even worse on other or older devices.

Imitate native app’s navigation in HTML

On the new YouTube app, we have a combination of a fixed header and off-canvas menu. When we pull out menu, the rest of the containers stays on place. We can easily swipe through the list in menu and swipe it off to hide it.

I wanted to create similar menu, which slides in from left side of the screen when I tap a logo and hide it when I tap it again.

It’s not very simple

We could use jQuery for the animation and depend on one simple click event listener, sure, it’ll do the job – but not the right way. Firstly, It already is a common knowledge that jQuery animations aren’t as fast and sleek as CSS3 animations. To make things even smoother we’re going to use CSS3 combination of translate3d() for setting A and B positions of the menu and transition for animating It. Using translate3d() function in our CSS file will also turn on hardware-acceleration trickery on the browser providing even smoother transitions. If you are concerned about compatibility you could always try animating left property instead (which I will cover later in this article), but keep in mind that It won’t be hardware-accelerated.

Secondly, click event doesn’t work as fast on mobile as on desktop – there is always ~300ms delay between the touch of our fingers and when the event is invoked. The browser is actually waiting to see it we’re trying to do a double-tap. Our app should be quick and responsive, so we’re going to use touchstart event instead. Below is a link to my test page showing difference between both click and touchstart events, be sure to try it out on mobile device. There is QR code inside.

Touchstart vs Click events test.

Lastly, the “scrollable” part. In this menu we’re going to use a combination of fixed elements with overflow scroll property, which is not very well supported on older Android devices. So as a fallback mechanism we can adopt nice iScroll script from cubiq.org and run it only when the browser says It’s old.

Turn theory to practise

Starting with HTML structure

html
head
/head
body
Yadda, yadda.
/body
/html

Explaination

Line 3 – setting meta viewport properties will make the page siutable for mobile devices. It locks maximum page size to device’s size and disables the possibility to zoom in with double-tap.

.nav-menu – I’ve used simple <div>s as menu list elements because when I tried <li>, they were changing heights while the whole animation was playing. If you’re concerned about SEO feel free to use proper navigation elements.

.header – is always visible and fixed to the top. It contains only “MENU” button, but you can play around and also add a search bar.

.wrapper – actual contents of our page: text, images and what-not.

Style it up

body{
  margin: 0;
  font-family: arial;
}
div{
  -webkit-box-sizing: border-box;
  -moz-box-sizing: border-box;
  box-sizing: border-box;
}
.header{
  position: fixed;
  left:0; right:0; top:0;
  height: 45px;
  
  background:#e0e0e0;
}
.menu-btn{
  font-weight: bold;
  padding: 10px;
}
.nav-menu{
  position: fixed;
  top:45px; bottom: 0;
  width:85%;
  
  overflow-y: hidden;
  overflow-x: hidden;
  
  background:#333;
  
  -webkit-transform: translate3d(-100%,0,0);
  transform: translate3d(-100%,0,0);
  
  /* easeOutCubic */
  -webkit-transition: -webkit-transform 0.25s cubic-bezier(0.215, 0.610, 0.355, 1.000);
  transition: transform 0.25s cubic-bezier(0.215, 0.610, 0.355, 1.000);
}
.menu-open .nav-menu{   
  overflow-y: scroll;
  
  -webkit-transform: translate3d(0,0,0);
  transform: translate3d(0,0,0);
}
.nav-menu div{
  padding: 10px;
  
  border-bottom: 1px solid #666;
  background: #333;
  color: #fff;
}
.wrapper{
  padding: 45px 1em 1em 1em;
}

Explanation

divbox-sizing is useful when dealing with containers that have inner padding. When we set our element’s width: 200px;, it will alwasy be 200 pixels wide, irrespective of the inner padding.

.header – as mentioned before, our header will be always glued to the top. Note that we’re setting the height here. It’s going to be .wrapper‘s top margin too, so the header won’t overlap the content (not even by a tiny pixel).

.nav-menu – here’s where the fun part begins. Menu should take 85% of the screen’s width when opened and displayed right below the header (hence the same 45px). The whole element is pushed outside the view thanks to lines 31-32 (setting x position to -100%). translate3D() is also used to force hardware acceleration for our page, which will make animations a bit smoother. transition property contains custom-made easing function. Ceaser is a nice tool I used for creating specific cubic-bezier CSS functions. In the line 38 there are definitions for the opened menu. Class .menu-open will be attached to and removed from body element every time the user taps menu button.

.nav-menu div – defines all menu items.

.wrapper – here we see again those 45px. Just like with the menu, we can’t display our content hidden under the header.

JavaScript Interaction

We’re almost there, the last thing to do is glue it all together with JS.

Variables

var webKitVersion = parseInt(
  navigator.userAgent.substr(
    navigator.userAgent.search("AppleWebKit/")+12, 3
  )
);
var menuOpen = false;
var navMenuScroll;

Variable name webKitVersion pretty speaks for itself, WebKit version is extracted from the user-agent. Then the script holds state of the menu in menuOpen and navMenuScroll will be used for the iScroll helper in case of older browsers.

Animation

Zepto(function($){
  loadCssPolyfill();

  $('.menu-btn')[0].addEventListener('touchstart', function(event) {
    $('body').toggleClass('menu-open');
    menuOpen = ($('body').hasClass('menu-open')) ? true : false;
  }, false);
  $('.wrapper')[0].addEventListener('touchstart', function(event) {
    if(menuOpen){
      $('body').toggleClass('menu-open');
      menuOpen = ($('body').hasClass('menu-open')) ? true : false;
    }
  }, false);
});

Line 1 – is a Zepto’s way of running script when page finished loading.

Line 2loadCssPolyfill() will be defined later, it will help us with scolling list on onlder devices.

Line 4 – defines what happens when we touch menu button.

Line 8 – in casewhen we have menu opened we can always close it by touching content of the page, just like in Android native apps.

Scrolling for Android 2.*

function loadCssPolyfill(){
  // Shortcut, using POJO instead of Zepto/jQuery for ultra speed!
  var $ = document;
  
  // Provide fallback solution for Android 2.* devices with old WebKit
  if( webKitVersion<=533 ){

    // Get the styles on the fly
    if (!$.getElementById('android2'))
    {
      var head  = $.getElementsByTagName('head')[0];
      var link  = $.createElement('link');
      link.id   = 'android2';
      link.rel  = 'stylesheet';
      link.type = 'text/css';
      link.href = 'css/android2.css';
      link.media = 'all';
      head.appendChild(link);
    }

    // Use iScroll on .nav-menu 
    navMenuScroll = new iScroll('nav-menu', {
      hScroll: false,
      hScrollbar: false,
      bounce: false,
      lockDirection: true,
      scrollbarClass: 'nav-menu-scrollbar'
    });
  }
}

As you can see, the script attaches new CSS file for Android 2.* devices. This file contains some fixes and also animation of the navigation using left property, because we already know that nice webkit transform3d() transitions wouldn’t work here.

Nicely done

This project is far from what you would want to show to the client. I’ve shown you just the basics so now It’s all up to you to beautify it!

See the DEMO

Again, this Demo should also be viewed on your mobile device, not desktop! The menu does not respond to click events, so abusing it with your mouse won’t help . In case of any problems or questions feel free to put them in the comments below. I’d be more than happy to answer.

More to read

If you’re eager to pump more knowledge into your brain you can have a look at those articles:

  • Carson (alias)

    This is awesome! Idk if its a bug or not but I will identify it just to help you. (I’m a noob to programming). Sometimes the content (on the right to menu) scrolls along with the menu thing you made.

    • Yeah, It’s a bug. I haven’t figured out how to make it stop when the menu is out. I’ve seen many websites used similar navigation but their menu scrolls always with the content
      Thanks for noticing!

      • vbarbas

        One solution to this problem would be to fix position the content. In addition You could discuss things like swipe to open/close the menu and the problems introduced by that. Maybe in an other article. Nice job on this one tho :).