Steve Taylor photo

Keyboard-accessible navigation

This is perhaps a bit of wheel re-invention, but a lot of my code is built from scratch so I can customise it easily. The issue is this: how to make simple drop-down navigation menus properly keyboard accessible?

I’ve usually been using CSS for navigation drop-downs – just a little tweak to the display value of the sub-menu when the parent link gets :hover or :focus. Seems leaner to do this than use JavaScript unnecessarily.

The problem is that while a keyboard-only user can tab into the main list of links, and sub-menus will be revealed when they approach one, when they tab again, they’re lost. I think the first link in the sub-menu may get focus, but then the parent link loses it, and the sub-menu disappears!

So, the solution has to be JS-powered. Here’s the code:

See the Pen Accessible drop-down menu by Steve Taylor (@gyrus) on CodePen.0

Have a look at the HTML. The ARIA attributes are included as per Terrill Thompson’s very useful post on keyboard-accessible menus. I’ve not tested on a screen reader – that aspect may need refining. They’re worth noting, though, as I do use the aria-expanded attribute in the CSS and JS, as a useful and semantic handle to grab hold of. The last bit of the CSS is most important – to make sure sub-menus are hidden when aria-expanded is set to false on its parent container.

The JS is where it all happens. A mouseover or focus event on a menu item with children reveals the sub-menu. But the corresponding mouseout / blur event sets a small delay before proceeding, and then only hides the sub-menu if a certain data attribute isn’t set on the .sub-menu-wrapper element. That data attribute is handled in the next two event handlers, which make use of jQuery’s focusin, attached to .sub-menu-wrapper. So in the split second when a keyboard user tabs from a parent to a child link, that child link gaining focus will trigger the event handler which sets the wrapper’s data-has-focus attribute to true. Then when the setTimeout code kicks in, the check fails, and the sub-menu isn’t hidden. When the user tabs past the final child link, then the sub-menu is hidden.

I’ve found some issues with IE (surely not!), although at present I’m not sure if they’re specific to the project I’ve developed this code in. The demo on Codepen above seems to work – I’ll post updates if I find improvement for poor old IE.

There’s probably a lot of refinements to mull over. For example, should the up/down arrows be usable to go through sub-menus? But I think this code supplies the foundations for most scenarios.

One closely related accessibility issue worth mentioning: make sure you include a proper ‘skip to content’ link! Otherwise this handy code which sends the tab focus through all the menus could become a tiresome obstacle. WordPress users may have noticed the ‘skip to toolbar’ link which appears on the front-end when you’re logged in and start tabbing. That’s the basic principle. The link should be hidden by default (using an accessible technique), and revealed when it gains focus. Finally, you may find this fix for issues in Chrome and IE with ‘skip to content’ links useful.

UPDATE 9/5/15: I’ve refined the JS to work better, particularly in IE. The main changes are: to attach the top-level focus/blur events to the links, not the list item elements; to add checks to the focusout event for the .sub-menu-wrapper, to see if another element inside sub-menu, or the parent link, has simultaneously gained focus; made use of a has-focus class on the top-level link, due to jQuery no longer supporting the hover pseudo-event/class.


  1. Dan Farrow avatar Dan Farrow

    hi Steve, I was having an issue in Firefox 37 on Ubuntu where tabbing from the first Child Page item made the menu lose focus entirely.

    I was wondering where the focus was going, so I saved a local version and added setInterval( function(){ console.log( document.activeElement ) }, 1000 ); to poll for the currently focussed element. Weirdly it turned out focus was switching to .

    But I just noticed that your update has fixed this, so my investigation has been cut short. Which is great because I should be going to bed anyway – I’m running a half-marathon in the morning :)


  2. Dan Farrow avatar Dan Farrow

    Weirdly it turned out focus was switching to

  3. Dan Farrow avatar Dan Farrow

    …switching to ‘body’

    (third time lucky?)

  4. Steve Taylor avatar Steve Taylor

    Ah, glad the update fixed it! Enjoy the run…

  5. Jean-Marc avatar Jean-Marc

    There is also an issue when you use shift+tab key combination : You can’t cycle anymore through submenu items.

  6. Steve Taylor avatar Steve Taylor

    Yep, minor issue. You can go back down the submenu once you’re back on the top-level link so not prohibitive. I’ve moved on from this code and not found time to post again so this code is remaining for reference.

Leave a comment

Your email address will not be published. Required fields are marked *