Responsive, Pure CSS Off-Canvas Hamburger Menu
Pure CSS off-canvas hamburger menus aren’t a recent discovery. After all, Chris Coyier wrote about this technique back in November of 2012.
- If this is an old trick for you, then hang with me for a bit. I’ve improved upon Chris’s example, and would love your feedback.
- If this is new to you, don’t worry. You have plenty of company, as it seems much of the web hasn’t actually caught on yet.
With that, we’re going to build a simple, responsive off-canvas hamburger menu using only CSS that will be easy to incorporate into your own project. But first…
- It’s never a good idea to ignore potential users.
- Progressive Enhancements is just good engineering.
Let’s do this!
Step 1: HTML
As you may know, the first step is always writing a solid, well-thought-out, base-layer of HTML.
Note: I’m using Font Awesome for the icons in my example.
Looks rather standard, right? We have:
- Our parent <header> element
- The hamburger (“fa-bars”) icon
- A main heading (or potentially a logo)
- The navigation in a <nav> element
- A close icon (“fa-close”) inside the navigation (more on this later)
- A “backdrop” after the navigation. Why is it an anchor tag? I’ll explain later.
Step 2: Let’s make it more accessible
With that, we’re going to add a few more attributes and some screen-reader-only text:
Here’s quick breakdown off all these attributes and how they function:
- We’re making the anchor tags appear as buttons to screen readers with [role=“button”]. Otherwise, they’d be normal links (which as a widget they shouldn’t be).
- We’ve added unique IDs for targeting our HREFs (more on how this works later).
- We’ve told screen readers that the hamburger menu is not expanded with [aria-expanded=“false”].
- We’ve provided an informative label of the buttons for screen readers using [aria-label].
- We’ve assigned the <nav> element an ARIA [role=“navigation”].
- We’ve hidden the icons from screen readers with [aria-hidden=“true”], because they’re visual representations, and added screen-reader-only text with the <span class=“sr-only”> elements.
- We’ve taken the “backdrop” out of the tabbing index with a [tabindex=“-1”]. It’s purely visual in nature and we don’t want to confuse our visually impaired and keyboard-only users.
- We’ve added the amazing [hidden] attribute to set the initial (and semantic) state of the “backdrop”. No more [class=“hidden”] garbage — how exciting!
That’s a lot, but it’ll be worth it — I promise.
Here’s the result so far:
Step 3: Let’s style it!
We’re going to approach this mobile-first, so let’s knock out the mobile, “hamburger-y” view (the interesting part).
First, we’re going to just get the layout of the header right (without the interactivity):
Step 4: Interactivity with pure CSS
When making widgets interactive with CSS, you have a couple options:
- Use radios or checkboxes
- Use the :target pseudo-class.
Radios and checkboxes work amazingly well for most widgets, like tabs, modals, dropdowns and accordions. Chris Coyier dubbed this technique “the checkbox hack.” Several developers have used this “hack” for their off-canvas menus, like in Paul Lewis’s tutorial for Chrome Dev Summit or Luis Manuel’s morphing hamburger menu.
However, the :target pseudo-class is more semantic in this use case, since we’re directly dealing with navigation. You might disagree, and that’s completely ok! It would be incredibly easy and perfectly acceptable to swap out the :target pseudo-class for a checkbox.
Either technique has its caveats, though.
Using a checkbox:
- Requires the <input> field to be a sibling of the menu or at least a sibling of the menu’s ancestor. In other words, the CSS is a bit trickier. You can have the <label> (even multiple labels) elsewhere, though.
- The <label> element will not be directly focusable or tab-able, requiring some slightly trickier CSS for handling the focus on the checkbox while changing the visible appearance of the <label>.
- The keyboard navigation around opening/closing the menu will be wonky. Affecting a state change on a checkbox is done through the [spacebar] not the [return] key. While blind users may understand that the widget is operated by a checkbox, sighted keyboard users will be confused since the checkbox is not apparent — something I felt was a deal breaker in this use case.
Using the :target pseudo-class:
And there may be other caveats I missed. Either way, choosing which technique is both a matter of preference and subject to your project’s requirements. Anyway, I’ve digressed…
Here’s the interactive part of the CSS:
The result when clicked:
How all this works
Essentially, the :target pseudo-class gives us a new “state” for styling the targeted navigation. When main-menu has been targeted (with its hash added to the URL) we can now slide out the menu. It’s a bit like a :focus pseudo-class for the targeted element (not the link itself).
We’ve also allowed the “backdrop” to display when the navigation is targeted.
I’ve added the @supports media query to provide the preferred position:fixed CSS to browsers (both mobile and desktop) that support it. Otherwise, lame browsers and devices — I’m looking at you iOS — will get position:absolute.
Step 5: Larger screen styles
Since we don’t want the hamburger menu to display for non-mobile devices (or larger screens in general), we’ll add the necessary media query for that. Then we’ll style it to look like a horizontal navigation:
Voila! We’re done!
Putting it altogether
Here’s the final HTML:
Here’s the final CSS:
Try out my CodePen for yourself:
Note: you can demo the checkbox version of the menu too.
Have other thoughts or suggestions?
I’d love to hear your comments with my approach to a pure CSS hamburger menu.