Pure CSS Off-screen Navigation Menu

Austin Wulf
Tweet

Hamburger menu, drawer menu, off-canvas menu: Whatever you call it, hiding a website’s primary navigation just off screen is becoming a ubiquitous pattern in responsive web design. More and more sites feature a fixed-position icon that, when tapped, pushes the entire site to the side to reveal a hidden navigation menu.

While there are plenty of jQuery plugins that will create this effect for you, it’s actually pretty easy to achieve without using any JavaScript at all.

This article will show you how to make a simple version of the off-canvas menu and sliding effect using only CSS.

Before we get started, I’d like to make a note that using this method for creating the drawer menu means your site’s navigation will always be in a drawer, regardless of viewport size. If you only want the drawer menu on smaller screens, you’ll have to use some JavaScript to manipulate the DOM order or use some funky CSS to make the menu look right on larger screens. In other words: It’s pretty and simple, but it’s not always the right solution for every situation.

Here’s a CodePen demo showing the end result:

See the Pen Pure CSS Off-Screen Navigation Menu by SitePoint (@SitePoint) on CodePen.

Start with Some HTML

The markup for our off-canvas menu is a bit different than your standard navigation menu. Instead of sticking it in the site’s header, we’re going to start right inside the <body> tag.

This is the basic structure:

<ul class="navigation">
    <li class="nav-item"><a href="#">Home</a></li>
    <li class="nav-item"><a href="#">Portfolio</a></li>
    <li class="nav-item"><a href="#">About</a></li>
    <li class="nav-item"><a href="#">Blog</a></li>
    <li class="nav-item"><a href="#">Contact</a></li>
</ul>

<input type="checkbox" id="nav-trigger" class="nav-trigger" />
<label for="nav-trigger"></label>

<div class="site-wrap">
    <!-- insert the rest of your page markup here -->
</div>

You can see our site’s markup is made up of three main elements: the navigation, a checkbox and label pair, and the site’s actual content.

A few things to note:

  • The navigation section is first in the source order because it’s “behind” everything else on the site. You can use whatever HTML tags you want to build the navigation. Here I’m using an unordered list, which is common.
  • The trigger to slide out our menu is a checkbox input with a label. Typically the label would come before the input or wrap around the input. In this case, the input has to come directly before the label. We’ll see why later when we add the CSS.
  • The rest of our site has to be wrapped in a unique div. This is so that when we open the menu, everything else can slide slightly off-screen to reveal the hidden navigation elements underneath.

Now that we’ve got our basic HTML structure, we can start making it look pretty!

The CSS for the Menu Items

Let’s start by styling the navigation menu and items. First off, we need to make sure our navigation menu is behind our page content and that it stays in place even if a user scrolls:

.navigation {
    list-style: none;
    background: #111;
    width: 100%;
    height: 100%;
    position: fixed;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 0;
}

Next, I’ve added some styles to make our navigation look snazzy (background colors, borders, gradients, etc.). I won’t reproduce the code here, but you can review the demo to check those out.

Now we have some nice looking menu items, but it doesn’t look so great with all of our content just laying on top of it. Let’s add some styling to hide the menu until we’re ready for it.

The CSS for the Site Wrapper

To start, let’s make sure the site’s content completely covers our menu. At this point, you may want to add a few paragraphs of lorem ipsum to your .site-wrap element, if you haven’t already added any content.

.site-wrap {
    min-width: 100%;
    min-height: 100%;
    background-color: #fff;
    position: relative;
    top: 0;
    bottom: 100%;
    left: 0;
    z-index: 1;
}

Note that we must specify a background on .site-wrap or else the menu will show through. You can, of course, use any kind of background you want. I added the following to mine:

.site-wrap {
    /* ...previous styles here... */
    padding: 4em;
    background-image: linear-gradient(135deg, 
                      rgb(254,255,255) 0%,
                      rgb(221,241,249) 35%,
                      rgb(160,216,239) 100%);
    background-size: 200%;
}

The CSS for the Menu Trigger

Next we’ll add the styles that change the menu trigger from a standard checkbox input into the classic “hamburger” icon that we all know and love.

First, let’s hide the checkbox.

.nav-trigger {
    position: absolute;
    clip: rect(0, 0, 0, 0);
}

Editor’s note: Originally, this code was using display: block along with zero width and height for the checkbox, to make it invisible but still accessible. It turns out, this combination was causing iOS to crash the browser when the menu was opened. I’ve changed the technique to use the clip property instead, which seems to have the same level of accessibility.

Here we are hiding the checkbox using the clip property, which requires that the element be set to position: absolute.

Now let’s style the <label> element:

label[for="nav-trigger"] {
    position: fixed;
    top: 15px;
    left: 15px;
    z-index: 2;
}

First, we set the label to position: fixed so that it stays in the same spot as the user scrolls. The top and left properties dictate how far from the edge of the viewport the icon will sit. We also make sure the trigger’s z-index is at least one higher than that of the .site-wrap element.

Next, we add additional declaratins to make the lable into a “hamburger” icon.

label[for="nav-trigger"] {
    /* ... previous styles here... */
    width: 30px;
    height: 30px;
    cursor: pointer;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' version='1.1' x='0px' y='0px' width='30px' height='30px' viewBox='0 0 30 30' enable-background='new 0 0 30 30' xml:space='preserve'><rect width='30' height='6'/><rect y='24' width='30' height='6'/><rect y='12' width='30' height='6'/></svg>");
    background-size: contain;
}

I’ve used inline SVG as a background image, but you can use any icon you want, including :before and :after pseudo elements to recreate the “hamburger” icon using pure CSS.

Notice I’ve also included cursor: pointer; to visually indicate interactivity with cursor-based input.

The CSS to Make the Trigger Work

Now that our menu, site wrapper, and trigger are all styled, let’s add the last few lines of CSS that make it all work.

.nav-trigger:checked + label {
    left: 215px;
}

.nav-trigger:checked ~ .site-wrap {
    left: 200px;
    box-shadow: 0 0 5px 5px rgba(0,0,0,0.5);
}

The second declaration block above ensures that the site wrapper is pushed to the right by 200 pixels. I also added a box shadow to the site wrapper to give it that extra visual feel of being stacked on top of the menu.

The first selector (.nav-trigger:checked + label) controls the position of the trigger when the menu is open. You’ll want to add the number we used earlier on label[for="nav-trigger"] to the amount you want the site wrapper to slide out. So in this case: 15px + 200px = 215px.

This is where the source order of the trigger elements becomes important. The second selector uses ~, the general sibling selector, to target .site-wrap when .nav-trigger is checked. The source order of our checkbox input isn’t as important here.

However, we have to target both .site-wrap and our <label> element based on whether or not our checkbox input is checked. To accomplish this, we use the + (adjacent sibling selector) to target the <label> element that’s next to the checked checkbox. If we put the label first, there’s no way to move it along with the site wrapper when we activate our trigger.

As a finishing touch, we can add a CSS transition to both the trigger and the site wrapper to open the menu with a smooth animation. Make sure to include any relevant browser-prefixed attributes in your version, or else use something like Autoprefixer.

.nav-trigger + label, .site-wrap {
    transition: left 0.2s;
}

One last thing: Make sure to hide any overflow on the x-axis of your <body>. Without this, your users will be able to scroll the whole window left and right when the menu is open.

body {
    overflow-x: hidden;
}

The Finished Product

And that’s it! We’ve successfully built a slick off-screen navigation menu without any JavaScript. Again, here’s the CodePen to demonstrate what it looks like when it all comes together:

See the Pen Pure CSS Off-Screen Navigation Menu by SitePoint (@SitePoint) on CodePen.

Now that you know how it’s done, feel free to play around with the idea. Make a version that slides in from the right, or make one that has both a left and right menu.

I’d love to see what you can come up with, so share a CodePen of your own design in the comments, or links to other examples of pure CSS off-screen navigation menus.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Jingqi Xie

    Although it requires more in DOM than the JavaScript method, it’s still worth trying.

  • http://ChiefAlchemist.com/ Mark Simchock

    Would the off-canvas bit be better to layer over the current page, as opposed to push it aside? Would that be less expensive in terms of pixels / redraw on a less powerful device?

    • http://austinwulf.com Austin Wulf

      This is a good question, and I think you’re right: It should take less work to redraw the menu overlaying the canvas instead of pushing the canvas aside.

      The reason I chose this animation specifically was because it’s a common pattern on native mobile apps, but I have done the “overlay” method on websites in the past. I haven’t tried the checkbox trigger with that animation style, but I have a feeling it could still be used there, as well.

      • http://ChiefAlchemist.com/ Mark Simchock

        Moi? Even with performance issues aside, I prefer an overlay. I personally don’t find shifting the whole main screen a smooth UX. Let the known stay known and layer on top without disruption. Of course there are exceptions.

        Do you know / think the above could be adjusted for overlay? If so, care to add that? Maybe? Please?

        • LouisLazaris

          If I get a chance, I’ll see if I can fork it to try the overlay method, but feel free to do it yourself and post it here! :)

    • mystrdat

      A dramatic difference actually, but then it’s a different effect.

  • Aaron Moreno

    This is cool, thanks for great sample. Going to use this for some mockups and prototype work.

    • http://austinwulf.com Austin Wulf

      Awesome, glad you enjoyed it!

  • Theracoon

    Works OK on My HTC One in Chrome but Crashed an iPhone 4S Safari both times I tried it. I was curious as I tried a similar method about 2 years ago but couldn’t get the iPhone to redraw sibling elements based on a targeted element.

    • LouisLazaris

      Hi @Theracoon:disqus,

      Thanks for pointing this out. I actually noticed this myself prior to your comment but I was trying to set aside some time to debug this before I posted a comment about it.

      It turns out, any browser on iOS was crashing (they all run on the same engine, even Chrome on iOS), because of the checkbox being given height and width values of “0”. I’ve changed the method for hiding the checkbox to now use position: absolute along with the clip property, which is supposed to be an accessible way to hide elements.

      So the crashing should no longer happen on iOS (iPhone or iPad) anymore. Thanks!

      • Theracoon

        Excellent, I’ll give it a try.

  • http://austinwulf.com Austin Wulf

    This is a great way to handle long menus with many items or short screens that can’t hold a handful of items. Thanks for the suggestion!

  • http://austinwulf.com Austin Wulf

    Thanks Scott! I love your example, I hadn’t thought of using a radio selector with multiple labels but that seems like the smarter way to achieve this. Kudos!

    • 醉拳

      Hello Austin,

      if the user scrolls down on the page and then opens the menu, the browser will jump back to the top of the page. I guess there is not much I could do to circumvent this, since this is intrinsic to the current design of your menu, right?

  • mystrdat

    You cannot watch for keyboard events from CSS and adding it as a JavaScript feature would obviously defeat the whole purpose of it.

    • LouisLazaris

      It should work with tabindex, no need for JS. But the problem is that the checkbox isn’t visible. The author and I are going to look into it and maybe we’ll update it if we can get it to take focus. It just doesn’t seem to be working now, due to the hidden checkbox.

      • mystrdat

        True tabindex will work, my mistake.

  • LouisLazaris

    @matebrki:disqus / @mystrdat:disqus / @austinwulf:disqus

    It looks like I made a mistake, the checkbox is accessible with the keyboard, it’s just that in order to select/deselect a checkbox with the keyboard, you have to focus it and the press the space bar. I forgot about that.

    So I guess the problem now is how to indicate to the user that the checkbox has been focused, and that the space bar toggles it. I think it might be possible to add a tooltip or something on focus, using CSS, to indicate that, but that might be too much.

    You can also maybe assume that a prolific keyboard user would know how to do this, but he won’t necessarily know it’s a checkbox unless it’s a screen reader or something that announces it, which I’m not sure of.

    So I think it is a doable solution, but it would have to be tested for accessibility before committing to using it.

    • exodus

      If you add accesskey attribute to the checkbox element (accesskey=”m”) then u can trigger the menu with [alt+m] or [alt+shift+m] depending on browser.

      • LouisLazaris

        True, and that will improve things, because the menu becomes universal with the keyboard, but that still doesn’t solve the problem of the user not knowing that the menu is accessible via the keyboard. But yeah, that is a better solution for sure, thanks.

        • James Edwards

          No it isn’t :-) Accesskeys cause more problems than they solve.

          Look into aria attributes instead — something which semantically indicates the role and state of the widget.

          • LouisLazaris

            @disqus_9w7PMRX3XV:disqus Thanks, that’s just my ignorance of accesskeys there. When I first read the suggestion, I thought accesskey was obsolete, but when I looked it up, it was still in the HTML5 spec. Any idea why it’s still valid if it causes problems? Do you have a source link explaining the issues? Care to write something on it for us? :)

    • heydonworks

      Just add

      #nav-trigger:focus + label {
      outline: thin solid;
      outline-offset: 3px;
      }

      • heydonworks

        Also, the label should have a text node or accessible name of some sort eg.

        <span class=”vis-hidden”>menu

  • LouisLazaris

    Ah, yes, this is a good point that I should have incorporated when I edited the article for Austin. I’ll see if I can do it and I’ll post an update here. Thanks, Dan!

    And for those interested, this is the proof of what Dan is talking about:

    http://www.paulirish.com/2012/why-moving-elements-with-translate-is-better-than-posabs-topleft/

  • http://www.realpointdesign.co.uk Ian

    Hi,

    Thanks for the article, really helpful but I’ve come unstuck when trying to move it on a step. I wanted to move the trigger inside the wrapper or at least into a containing DIV so it can be hidden when viewing on non-mobile. But as soon as I wrap it in a DIV even outside of the wrapper it stops it working. What am I not understanding?

  • http://aarontgrogg.com/ Aaron T. Grogg

    This is a great technique! However, I notice that if I scroll the content, then open the menu in the above example, the content jumps back to the top… Scott O’Hara’s example does *not* do that, possible to implement the difference?

    • Floris

      you can change the positioning on the .nav-trigger from absolute to fixed, that should do the trick.

    • Gideon de Vries

      .navigation{
      overflow:auto;
      overflow-y:scroll;

      }

    • Gideon de Vries

      Also use this:
      .site-wrap{
      overflow-x: hidden;
      }

  • CTN

    solid stuff. good work.

  • 醉拳

    Hello Scott, I really like your innovative menu button solution. Do you have any browser support info for it, the GitHub link on your website (bottom end of the post) seems to be broken, I guess you have some more technical info on the GitHub description page.

    Thanks and kudos!

    • http://scottohara.me/ Scott O’Hara

      the github repo doesn’t actually have more information in it (and the link you referenced was actually for my website, but that has gone private, hence it breaking)

      The demo works in any browser that supports the :checked, + & ~ css selectors. The animation requires css transitions/transforms to be supported Basically IE9/10+ and modern versions of FF, chrome and safari.

  • angel

    Hi! this is pretty cool! I just want to know how do you use this for a parallax website? I tried using it and whenever i press the burger icon, the page goes back to the top

  • Udo ツ Springfeld™

    Awesome, thanks. If you use base64 encoding, it works in IEMobile too:
    http://codepen.io/springfeld/pen/qEziA/

  • Gideon de Vries

    Hi you all, first of all, many thanks for your handy off canvas menu. I do recommend to use overflow-x: hidden; in .site-wrap. because otherwise the user can swipe horizontal. We wont want that.

  • Gideon de Vries

    Austin Wulf I was wondering if its possible to make a background bar (width 100%) in the nav trigger?

  • AndieR

    Really solid work, what would it take to make the menu stay put at the top of the page rather than on the side?