Article

Responsive CSS3 Multi-Level, Drop-Down Menu (with JavaScript/SwiftClick enhancement - no 300ms delay + tap-to-close drop-downs)

This responsive menu uses JavaScript to beat the 300ms delay on touch devices, close dropped-down sub-menus on touchscreen with a tap anywhere in the surrounding area, and close all the menus/sub-menus in one go in mobile view. These are improvements on the CSS-only version where you have to close all opened sub-menus in turn, and on the previous JavaScript version which only includes the "X-to-close-all-subs" JavaScript fix. Please read the Notes section for details about JavaScript enhancement.

Desktop (> 960px): You'll see a familiar-looking horizontal, drop-down menu bar at the top of the screen...

Mobile (< 960px): Give that    icon in the top-right corner a click to open the "off-canvas" menu...

Features

A CSS3, responsive menu that;

* Although an infinite number of nested sub-menus is possible, you'll probably want to stop at 2 or 3 deep due to sub-menus progressively flying right in desktop view (see the "Fly-Left" section below for workaround).

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

Fly-Left (desktop view)

OK, so sometimes you'll need a fly-out sub-menu from a button that's already close to the right-hand edge of the screen. No problem; a "fly-left" option is provided to reverse the direction of the fly-outs. The "JemCon.org > Previous JemCons" menu is an example of this.

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

<li class="fly-left"><a href="#">Previous JemCons</a> <label for="jemcon-previous" class="toggle-sub" onclick="">&#9658;</label>
<input type="checkbox" name="nav" id="jemcon-previous" class="sub-nav-check"/>    
 <ul id="jemcon-previous-sub" class="sub-nav">
    <li class="sub-heading">Previous JemCons <label for="jemcon-previous" class="toggle" onclick="" title="Back">&#9658;</label></li>
    <li><a href="#">JemCon 2013</a></li>
    <li><a href="#">JemCon 2012</a></li>
...

You can actually reverse the fly-out direction half-way through a series of nested sub-menus too, however, please be aware that on iPad (where the desktop menu is also likely to be visible), this can lead to the reversed fly-out sub-menus obscuring parent menu buttons. On a desktop PC this isn't a problem because the menus hide again once you hover away, but on touch-devices, hovers are replaced with clicks/taps so you cannot hover away to hide the menu. The only way to close an opened menu is to click/tap on another link.

Go-Up (desktop view)

Long, nested drop-down 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 drop-downs. 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 the 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;

<script>
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
        }     
    }
</script>

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 to beat the 300ms delay and close drop-downs on large screen touch devices)

One of the most annoying things on touch devices is the way that CSS hover activated menus stay open until another link is clicked. Well, this example fixes that - view it in a large screen touch device (iPad or other large tablet) and you can just click the surrounding area to close any open menus. Great stuff! "How did you do it?", I hear you ask. Well, to be honest, it was something I stumbled on while researching perceived performance on mobile (and ways to improve it).

The first point in the article mentions a JavaScript function called FastClick by FT Labs, which removes that 300ms delay from web buttons/links. This sounded brilliant, so I downloaded it and started playing around. Unfortunately, I noticed some strange behaviour on my CSS menus, where FastClick caused sub-menus to momentarily vanish after activation, and then reappear, or worse still, it caused the wrong drop-down to open! Huh? It was all very perplexing. Now, I'm not saying that there's anything wrong with FastClick - far from it - my problems probably have more to do with the way I implemented it. But still, these issues prompted me to seek out a fix, which is how I came across an alternative to FastClick called SwiftClick. In their Faster Tapping with SwiftClick article, the author mentions the same problems that I'd noticed too;

"For the most part this [FastClick] worked really well, but we eventually began to find that the util sometimes exhibited strange behaviour, such as events not firing when links were clicked and then firing later on, at the same time as other click events when different elements were clicked."

Well, at a 5th of the script size, (5kb instead of 25kb non-minified), I thought I'd give SwiftClick a whirl. I downloaded the minified code, dropped it into the bottom of my page (this demo), and initialised it, as directed, on the document body;

var swiftclick = SwiftClick.attach(document.body);

By default, the SwiftClick script only targets common clickable elements (a, div, span, button), which is what makes it smaller than FastClick, but I want it to work on my menu's arrow labels too. With SwiftClick you can add more elements;

swiftclick.addNodeNamesToTrack (["label"]);

If you try this demo on a touch device, and compare it to the earlier version without SwiftClick you will see that the sub-menus activate in ultra-quick time, AND the drop-downs in desktop view close again without obscuring content! Oh, happy day, and big kisses to the SwiftClick team!

Update December 2015: See the iOS 'Sticky Hover' fix.

Compatibility

* IE8 and under does not appear on any mobile device so the fact that the "off-canvas" mobile menu doesn't work there isn't really a problem. IE8 doesn't support media queries either so you can just serve an IE8 desktop stylesheet (with the desktop menu CSS) to those visitors.

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

Related

Freebies

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.