Pure CSS Off-screen Navigation Menu

Pure CSS Off-screen Navigation Menu.

Published by Austin Wulf

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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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:

1
2
3
4
5
6
7
8
9
10
11
12
.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.

1
2
3
4
5
6
7
8
9
10
.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:

1
2
3
4
5
6
7
8
9
.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.

1
2
3
4
.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:

1
2
3
4
5
6
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.

1
2
3
4
5
6
7
8
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.

1
2
3
4
5
6
7
8
.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.

1
2
3
.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.

1
2
3
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:

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.