Responsive CSS3 Multi-Level, Fly-Out Menu (with JavaScript enhancement + keyboard 'tab-to' support)

Here is another responsive menu that adds a desktop layout to the earlier CSS3 Multi-Level, Off-Canvas Mobile Menu (no JavaScript).

This menu differs from the CSS-only version in that it uses a smidge of JavaScript to make sub-menus accessible via the TAB key on a keyboard in desktop view, and close all the menus/sub-menus, and hide the off-canvas menu again, in one click, in mobile view. In contrast, the CSS-only version cannot have sub-menus tabbed-to, and it requires all opened sub-menus to be closed in turn, before the user is able to send the menu back off-canvas and bring content back in to view. Please read the Notes section for details about JavaScript enhancement.

Desktop (> 960px): Look left to see the always-visible vertical menu, and hover/tap to trigger subs...

Mobile (< 960px): Click/tap that    icon in the top-right corner to open the "off-canvas" menu...


A CSS3, responsive menu that;

  • Supports unlimited sub-menus, of infinite depth *
  • Uses a nested list format
  • Uses CSS3 transitions to animate the "off-canvas" slide effect (no JavaScript)
  • Uses the "advanced checkbox hack" to activate "off-canvas" sub-menus (for Android/iOS)
  • Uses "translate3d" to force hardware acceleration in WebKit (no flicker)
  • Has a "Go-Up" option to change the vertical direction of fly-out sub-menus
  • Is only slightly enhanced with JavaScript and degrades gracefully with it turned off

* Although an infinite number of nested sub-menus is possible, you'll probably want to stop at 2 or 3 deep due to the sub-menus that progressively fly across the screen in desktop view.

Please view the source of this web page to grab the JS, CSS and HTML markup.

Go-Up (desktop view)

Long nested menus will eventually fall off the bottom of the screen. To counter this, a "go-up" option is available to change the vertical direction of sub-menus. In otherwords, to make them go upwards. The "Focus on Function > Services" menu is an example of this.

To trigger the "go-up" menu, just put a class of "go-up" on the parent <li> tag;

<li class="go-up"><a href="#">Services</a> <label for="fof-services" class="toggle-sub" onclick="">&#9658;</label>
<input type="checkbox" name="nav" id="fof-services" class="sub-nav-check"/>    
 <ul id="fof-services-sub" class="sub-nav">
    <li class="sub-heading">Services <label for="fof-services" class="toggle" onclick="" title="Back">&#9658;</label></li>
    <li><a href="#">Content Management</a></li>
    <li><a href="#">Graphic Design</a></li>

CSS Transforms and the "position:fixed" bug in Safari

When CSS Transforms are used (as this menu does), there is a known buggy behaviour in Safari that affects child "position:fixed" elements. What happens is that it seemingly forces them to adopt a "position:absolute" placement, while forcing their immediate parents to "position:relative". This causes some very strange (and frustrating) results. There is an easy fix here though - apply this CSS if you have any "position:fixed" elements inside ".container";

/* #### - corrects 'unfixing' bug in Safari - #### */
@media screen and (-webkit-min-device-pixel-ratio:0) { .container { -webkit-transform:none !important } }

Notes (JavaScript enhancement for mobile view)

The first change to the CSS-only version of this responsive menu is that all the checkboxes are named, e.g;

<input type="checkbox" name="nav" id="something-unique" class="sub-nav-check" />

And the bit of JavaScript below is used to remove all checks from checkboxes, thus closing all of the menus/sub-menus at once (in mobile view) instead of needing to close each in turn. I've also included a snippet that adds a class of "js" to the <html> element when JavaScript is enabled which, when coupled with the appropriate CSS, makes the menu degrade back to the CSS-only version when JavaScript is turned off;

document.documentElement.className = 'js'; // adds class="js" to <html> element
function uncheckboxes(nav){
    var navarray = document.getElementsByName(nav);
    for(var i = 0; i < navarray.length; i++){
        navarray[i].checked = false

The JavaScript function is called inside an additional  ×  label, placed just above the closing #menu div, which closes the menu and sends all mobile navigation back off-canvas. Note that there is no "for" attribute in the label - for some reason, things are a bit buggy with it included;

    <label class="toggle close-all" onclick="uncheckboxes('nav')">&times;</label>
</div><!-- closing "#menu" -->

Now to make the new  ×  label look pretty, and also move the large "back"   arrows out of the way, add the following CSS to the mobile area of the stylesheet. Note the ".js" selectors that only apply when JavaScript is enabled;

#menu .close-all { display:none }
.js #menu .close-all { display:inline }
.js #menu .toggle { top:0; z-index:9999 }
.js #menu .sub-nav .toggle { left:0.15em; width:1em }

Finally, hide the  ×  label when in desktop view by adding this to the desktop CSS;

.js #menu .close-all { display:none }

Notes (JavaScript enhancement for keyboard tab support on desktop)

Problem: :hover activated CSS menus can only have their first-level buttons tabbed-to. While sub-menus can be momentarily revealed when a parent receives :focus, they will be hidden again once the user tabs off the parent button (when it loses focus). You can see the lost :focus issue here.

Solution: The JavaScript at play here adds an ".active" class to buttons that have sub-menus, when they receive focus. This class is only removed when the previous or next sibling button is tabbed-to. Note that this menu does not currently include ARIA attributes, which would further improve accessibility for screen readers.

In order for the keyboard tabbing aspect to work, the following changes are in effect from the CSS-only version of the responsive menu;

  • :focus and .active styles have been added to the CSS - to make them visible when tabbed-to
  • tabindex="0" has been added to the main <ul> element - to hide any visible sub-menus when tabbed past
  • tabindex="-1" has been added to all the <input type="checkbox"> elements - to stop them being tabable
  • id="menu" has been added to the menu's container element - the "#menu" selector is used in the script


  • Desktop: IE7+ (no fly/fade effect in IE7/8/9, no tab support in IE7/8) / Mobile: IE9+ (no slide effect in IE9)
  • Chrome
  • Safari
  • Firefox
  • Opera
  • Android
  • iOS

Please be aware that touch screen desktop/tablet users have to tap the indicator arrows to open sub-menus, and fly-out sub-menus are likely to obscure page content until another link is clicked (see the iOS 'Sticky Hover' fix).

I haven't been able to test in other browsers/devices but feedback is always welcome. Contact me if anything goes awry in other browsers.



Looking for more freebies for your website? Grab a bunch of free PHP, CSS and JavaScript goodies, from flat file CMS' to RSS managers to responsive CSS menus, galleries and sliders.