Responsive CSS3 Multi-Level, Push/Slide Menu (with JavaScript enhancement + keyboard 'tab-to' support)

This responsive menu provides a permanently visible desktop menu to compliment the earlier CSS3 Multi-Level, Off-Canvas Mobile Menu (with JavaScript enhancement).

It builds on the CSS-only version by using a snippet of JavaScript to make sub-menus accessible via the TAB key on a keyboard, and close all the menus/sub-menus in one go. This tempers the CSS-only behaviour where sub-menus cannot be tabbed-to, and you have to close all opened sub-menus in turn to either return to the main menu in desktop view, or send the menu back off-canvas in mobile view. Please read the Notes section for details about JavaScript enhancement.

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

Mobile (< 960px): Give that    icon in the top-right corner a click 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)
  • 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 alternative fly-out menu for IE7/8 where sub-menus progressively fly to the right across the screen.

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

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 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
    hideX(); /* hide the X label */

The JavaScript function is called inside 2 additional  ×  labels, placed either side of the closing #menu div, which close the menu and send all sub-menu navigation back off-canvas. Note that there is no "for" attribute in these labels - for some reason, things are a bit buggy with it included;

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

OK, I know the 2 extra labels above look a little hacky, but Chrome threw me a bit of a curveball in the way it handles z-index, so the second one is needed to make it visible on Chrome in desktop view. IE and Firefox are fine with just the first label - it always sits on top - but annoyingly, sub-menus obscure that one in Chrome. I have since resigned myself to the idea that one is needed for the mobile menu and one is needed for the desktop menu.

To make the new  ×  labels 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, and also note that "#dt-close-nav" is not being displayed at all in mobile;

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

Finally, hide both  ×  labels when in desktop view by adding this to the desktop CSS - the JavaScript functions underneath will display the desktop "#dt-close-nav" label again when needed;

.js #close-nav, .js #dt-close-nav { display:none }
.js #dt-close-nav { position:absolute; z-index:9999; top:0; left:3.15em; cursor:pointer; color:#fff; padding:0 0.25em; font:3.125em/1.375em Arial }
.js #dt-close-nav:hover { color:#ccc }

Notes (JavaScript enhancement for desktop view)

A little bit of extra JavaScript is needed to clean-up the UX in desktop view;

function showX(){ document.getElementById('dt-close-nav').style.display='inline' }
function hideX(){ document.getElementById('dt-close-nav').style.display='none' }

These 2 short functions are used to show and hide the  ×  label at appropriate times during menu navigation;

  • showX(); shows the  ×  label only when a sub-menu is open.
    It is only attached to the "onclick" event of first-level   labels that have a ".toggle-sub" class.
    (i.e. only on main-menu list items that have nested sub-menus)
  • hideX(); hides the  ×  label when all sub-menus are closed.
    It is only attached to the "onclick" event of second-level   labels inside <li>s that have a ".sub-heading" class.
    (i.e. only on the first sub-menu list items - second and third nested sub-menus don't need it)
    <li><a href="/">Focus on Function</a> <label for="fof" class="toggle-sub" onclick="showX()">&#9658;</label>
        <input type="checkbox" name="nav" id="fof" class="sub-nav-check"/>
            <ul id="fof-sub" class="sub-nav">
                <li class="sub-heading">Focus on Function <label for="fof" class="toggle" onclick="hideX()" title="Back">&#9668;</label></li>

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+ (alternative fly-out menu, no tab support, in IE7/8) / Mobile: IE9+ (no slide effect in IE9) *
  • Chrome
  • Safari
  • Firefox
  • Opera
  • Android
  • iOS

* IE8 and under is covered with an alternative fly-out menu, while IE9 and above gets the checkbox push/slide version along with all other modern browsers.

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



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.