Off-Canvas Mobile Navigation with CSS

off-canvas

A tutorial about how to create a mobile navigation component for a responsive website with a slide out animation

Commonly found in mobile apps, this UI component is an effective way of presenting a nav menu of small screens and can be easily implemented at a given breakpoint using media queries. We will use CSS for styling and animation and jQuery for adding/removing classes to trigger the animations.

View demo    View on Github

All CSS in this tutorial is written in SASS. If you haven't yet made the switch to use a CSS preprocessor, I can't recommend it highly enough. If you really need to see the compiled CSS for this tutorial, see the style.css file in the Github repo.

Step One: the HTML

<body class="loading">

<div class="wrapper">
<div class="inner-wrapper">
	
	<header role="banner">
		<h1>Title</h1>
		<button id="nav-toggle" aria-hidden="true">Navigation</button>

		<div class="nav-bg">
			<nav role="navigation">
				<ul>
					<li><a href="#">Nav Item</a></li>
					<li><a href="#">Nav Item</a></li>
					<li><a href="#">Nav Item</a></li>
				</ul>
			</nav>
		</div> <!--.nav-bg-->
	</header>

	<div role="main">
		<!--content-->
	</div> 

</div> <!--.inner-wrapper-->
</div> <!--.wrapper-->

<script>
// jQuery goes here
</script>

</body>

We have two main content divs, .wrapper and .inner-wrapper. The outer .wrapper is what we will attach the slide animation to, while the .inner-wrapper allows us to hide the overflow content when the slide animation occurs.

The header markup is typical, however we include a button which will be used to trigger the slide in/slide out animation of the navigation.

Step Two: CSS for layout

We start with default layout and navigation style which will apply to the full size view of the site. Note the .nav-open class, which will be added by jQuery later in this tutorial.

// SASS variables
$darkGrey: #2c3e50;
$darkerGrey: darken($darkGrey, 3%);

.inner-wrapper {
	position: relative;
	overflow-x: hidden;
}

.nav-open .inner-wrapper {
	overflow: hidden;
}

nav {

	ul {
		font-size: 0;
		text-align: center;
	}

	li {
		@include inline-block;
		list-style: none;

		a {
			color: white;
			display: block;
			font-size: 16px;
			text-transform: uppercase;
			text-decoration: none;
			padding: 10px 13px;
		}

		a:hover {
			background: $grey;
		}

		a:visited {
			color: white;
		}
	}

}

#nav-toggle {
	display: none;
}

Step Three: CSS for Off-Canvas navigation

Next we add the mobile navigation style, nested inside a media query targeting a viewport width under 1024px. This includes the styling of the <button> element with a nav icon.

@media only screen and (max-width : 1023px) {

.nav-bg {
	background: $darkerGrey;
	width: $navWidth;
	height: 100%;
	position: fixed;
	top: 0;
	left: -$navWidth;
}

.nav-open .nav-bg {
	overflow-y: auto;
}

#nav-toggle {
	background-image: url("data:image/svg+xml;charset=US-ASCII,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22utf-8%22%3F%3E%0A%3C%21--%20Generated%20by%20IcoMoon.io%20--%3E%0A%3C%21DOCTYPE%20svg%20PUBLIC%20%22-//W3C//DTD%20SVG%201.1//EN%22%20%22http%3A//www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd%22%3E%0A%3Csvg%20version%3D%221.1%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20xmlns%3Axlink%3D%22http%3A//www.w3.org/1999/xlink%22%20width%3D%2221%22%20height%3D%2224%22%20viewBox%3D%220%200%2021%2024%22%3E%0A%09%3Cpath%20d%3D%22M20.571%2018v1.714q0%200.348-0.254%200.603t-0.603%200.254h-18.857q-0.348%200-0.603-0.254t-0.254-0.603v-1.714q0-0.348%200.254-0.603t0.603-0.254h18.857q0.348%200%200.603%200.254t0.254%200.603zM20.571%2011.143v1.714q0%200.348-0.254%200.603t-0.603%200.254h-18.857q-0.348%200-0.603-0.254t-0.254-0.603v-1.714q0-0.348%200.254-0.603t0.603-0.254h18.857q0.348%200%200.603%200.254t0.254%200.603zM20.571%204.286v1.714q0%200.348-0.254%200.603t-0.603%200.254h-18.857q-0.348%200-0.603-0.254t-0.254-0.603v-1.714q0-0.348%200.254-0.603t0.603-0.254h18.857q0.348%200%200.603%200.254t0.254%200.603z%22%20fill%3D%22%23ffffff%22%20/%3E%0A%3C/svg%3E%0A");
	background-color: transparent;
	background-size: 24px 24px;
	display: block;
	width: 24px;
	height: 24px;
	text-indent: -9999em;
	border: none;
	outline: none;
	position: absolute;
	top: 20px;
	left: 10px;
	cursor: pointer;
}

nav {

	ul {
		text-align: left;
	}

	li {
		display: block;

		a {
			color: white;
			background: $darkGrey;
			display: block;
			font-size: 16px;
			text-transform: uppercase;
			text-decoration: none;
			padding: 10px 13px;
			border-top: 1px solid $grey;
			border-bottom: 1px solid darken($darkGrey, 3%);
		}

		a:hover {
			background: $grey;
		}
	}

    li:first-child a {
		border-top: none;
    }	
}

} // end media query

Step Four: the CSS3 animation

To create the slide in, slide out animation, we now add in the CSS3 keyframe animation. Using the transform(translateX) takes advantage of modern browsers' built-in hardware acceleration for a noticably smoother slide effect than if we used the transition property on left values.

There are two keyframe animations defined; slideOpen is triggered when the navopen class is added and slideClosed is triggered when this class is removed. To prevent slideClosed from triggering on page load, we target the loading class (which is added to the body element in the markup) and reset the animation to none.

Note: The syntax below uses mixins from the Bourbon library, which takes care of all the vendor prefixes.

@include keyframes(slideOpen) {
	from {
		@include transform(translateX(0));
	}
	to {
		@include transform(translateX($navWidth));
	}
}

@include keyframes(slideClosed) {
	from {
		@include transform(translateX($navWidth));
	}
	to {
		@include transform(translateX(0));
	}
}

.nav-open .wrapper {
	@include animation-name(slideOpen);
	@include animation-duration(.3s);
	@include animation-timing-function(ease-out);
	@include animation-fill-mode(forwards);
	@include prefixer(backface-visibility, hidden, webkit moz spec);
}

.wrapper {
	@include animation-name(slideClosed);
	@include animation-duration(.2s);
	@include animation-timing-function(ease-in);
	@include animation-fill-mode(forwards);
	@include prefixer(backface-visibility, hidden, webkit moz o spec);
}

.loading .wrapper {
	@include animation(none);
}

Step Five: the jQuery

Last we add the jQuery to facilitate the adding and removing of CSS classes. We start by setting up some variables for the body and .wrapper elements, the button and also storing the height of the viewport upon page load.

When the button is tapped, the loading class is removed and the nav-open class is added. Each subsequent button tap will toggle nav-open. If nav-open is applied, the page has the height set to the current viewport height, which, combined with the overflow-y: hidden property in the CSS, will prevent scrolling of the page when the navigation is open.

Additionally, we add a click event to the <div role="main"> which closes the nav.

var body = $('body'),
	page = body.find('.wrapper'),
	navToggle = body.find('#nav-toggle'),
	viewportHt = $(window).innerHeight();

navToggle.on('click', function(){
	
	body
		.removeClass('loading')
		.toggleClass('nav-open');

	if ( body.hasClass('nav-open') ) {
		page.css('height', viewportHt);
	} else {
		page.css('height', 'auto');
	}	

});

page.find('[role="main"]').on('click', function(e){
	body.removeClass('nav-open');
	e.preventDefault();
});

Step Six: Internet Explorer support

We couldn't have a nice UI pattern without needing an IE hack, could we? Thanks to Vlad's comment below which pointed out a failure to work in IE10, I have added this section with a fallback to support IE10 & IE11.

Firstly, we need to target IE. Conditional comments are gone as of IE10, but we can use a media query to achieve the same thing. Credit to Alex Kloss and Keith Clark for this snippet:

@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) and (min-width:0\0) {
  // next, we'll add the IE styles here
}

Although keyframe animations are supported in IE10/11, for some reason I am yet to debug, the animation in this tutorial still fails. However, we can still make the open/close action work without animation using transform/translateX, as follows:

@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) and (min-width:0\0) {

  $navWidth: 240px;

  .nav-open .nav-bg {
  	left: 0;
  }

  .wrapper,
  .nav-open .wrapper {
  	@include animation(none);
  }

  .nav-open .wrapper {
  	@include transform(translateX($navWidth));
  }

  .wrapper {
  	@include transform(translateX(0));
  }

}

I've found that the left position behaves differently in IE, so this has also been adjusted to make the menu usable.

View demo    View on Github

12 Responses to “Off-Canvas Mobile Navigation with CSS”

  1. Sam says:

    Thanks for posting. Your link to github repo is wrong though..

  2. Vlad says:

    Great code, thank you. However, I tried to re-size window on IE10 to test it, but it’s not working. I’m afraid that this code will not work on phones and tablets under Windows. Do you maybe plan to add support for IE as well?

    • Tim says:

      Thanks for alerting me to the failure of this code to work in IE10. I have added a new section to this tutorial with a fallback specifically for supporting IE10 & IE11. I hope to soon debug why the keyframe animation doesn’t work, but for now I’ve created sometime to simply allow the menu to open and close.

  3. Andrew says:

    Hi Great tut. One thing I noticed on Android mobile (kitkat) was when you open the nav then close, you can then swipe from the right side of the page to left and there is the same amount of white space appearing as the left navs width. Probably from changing the size of the viewport

    • Tim says:

      Hi Andrew,

      I too noticed this bug and I haven’t yet been able to solve it. I had thought it was Android ignoring the overflow:hidden property when the nav-open class is applied, however I had not considered the viewport width in terms of the meta viewport property, such as what can happen on orientation change. There might be a similar solution in JavaScript to reset meta name="viewport" content="width=X" when the nav closes. If I find one I’ll be sure to update this post and the Github repo.

  4. Cory Miller says:

    Tim & Andrew: Ran into this problem as well, was able to correct it by adding overflow-x:hidden; to the body tag.

  5. Brett says:

    Hi

    How do you link the menu items to a different page

  6. adam says:

    For current Chrome you need this fix:
    @keyframes slideOpen {
    from {
    -webkit-transform: translateX(0);
    transform: translateX(0);
    }
    to {
    -webkit-transform: translateX(240px);
    transform: translateX(240px);
    }
    }
    @keyframes slideClosed {
    from {
    -webkit-transform: translateX(240px);
    transform: translateX(240px);
    }
    to {
    -webkit-transform: translateX(0);
    transform: translateX(0);
    }
    }

  7. Sandra says:

    Thank you again for this awesome work.

  8. strony opole says:

    Thanks so much. I will using this menu in my projects

Leave a Comment