Creating accessible JavaScript menu systems – some basic steps that get forgotten
OK, so you want to write yet another collapsing menu/section/widget thingamabob. This is really easy and in the following few examples I want to point out some ideas that make it much easier for you.
The following is what we’re going to build – it doesn’t have any bells and whistles like smooth animation, random unicorns, swoosh noises or whatever else you might want to come up with, but it is a good solid base to work from.
Click the image to go to the live example.
The Markup
The first trick is to use easy to understand and highly styleable markup for your widget. You see a lot of widgets that use endless DIVs, SPANs, IDs on every element and classes on every sub-element. If you build a simple solution (and not a catch-all reusable widget that is part of a framework) none of that is needed. In this case the markup is the following:
<ul id="menu">
<li><h3>Section 1</h3>
<ul>
<li><a href="foo.html">Foo</a></li>
<li><a href="bar.html">Bar</a></li>
<li><a href="baz.html">Baz</a></li>
</ul>
</li>
<li><h3>Section 2</h3>
<ul>
<li><a href="foo.html">Foo-1</a></li>
<li><a href="bar.html">Bar-1</a></li>
<li><a href="baz.html">Baz-1</a></li>
</ul>
</li>
<li><h3>Section 3</h3>
<ul>
<li><a href="foo.html">Foo-2</a></li>
<li><a href="bar.html">Bar-2</a></li>
<li><a href="baz.html">Baz-2</a></li>
</ul>
</li>
<li><h3>Section 4</h3>
<ul>
<li><a href="foo.html">Foo-3</a></li>
<li><a href="bar.html">Bar-3</a></li>
<li><a href="baz.html">Baz-3</a></li>
</ul>
</li>
</ul>
In other words: a nested list with headings for the different sections. This makes sense without styling or scripting which is still and important point considering mobile devices or environments that have JavaScript turned off (and yes, they are not mythical but do exist).Assistive access does not mean lack of JavaScript
The first mistake that people do is to think that this is all you need to be accessible – as users with assistive technology have no JavaScript, right? Wrong. Assistive technology is not a browser replacement but in most cases hooks into browser functionality and offers easier access. In other words, what the browser gets the assistive technology gets and you have to make sure it still makes sense without a mouse (to name just one example).
Thinking too complex
The next extra step people take is starting to loop through the menu construct to hide the elements with JavaScript. This is made much easier with clever libraries that don’t use the DOM to do that but piggy-back on the CSS selector engine, but for good old IE6 this is still not an option. And you don’t need to. In order to hide the nested lists in the above example all you need to do is to apply a class to the main list and leave the rest to CSS:
<style type="text/css" media="screen">
#menu.js ul{
position:absolute;
left:-9999px;
}
</style>
<script type="text/javascript" charset="utf-8">
var m = document.getElementById('menu');
m.className = 'js';
</script>
This means that in order to show any of the nested lists, all you need to to is to add a class called “open” to the parent list item. The CSS to undo the hiding is the following:
<style type="text/css" media="screen">
#menu.js li.open ul{
position:relative;
left:0;
}
</style>
You can add the classes on the backend when you render the page – which makes a lot more sense to indicate a current open section of the page than any JavaScript analyzing of window.location or other shenanigans. See the first stage demo. You can also define a “current” class which means you can style it differently and the script will simply quietly skip that section and not collapse it at all.
That is the beauty in leaving all the showing and hiding to the CSS. Of course you can’t animate in CSS (unless you use webkit), but you can make your animation precede the change in classes.
Hiding and showing
So the way to show and hide things is simply to add and remove classes. We need to use event handling to listen for click events to do that. Click events are the only safe ones as they fire with keyboard and mouse. The problem is that a header can be made clickable, but is not keyboard accessible. To work around this, let’s use DOM scripting to inject a button element into the headers. You can use CSS to make it look not like a button:
<style type="text/css">
h3 button{
border:none;
background:transparent;
}
</style>
The second stage demo shows how this works and here’s the code.
(function(){
// get the menu and make sure it exists
var m = document.getElementById('menu');
if(m){
// add a class to allow the CSS magic to happen
m.className = 'js';
// loop over all headers
var headers = m.getElementsByTagName('h3');
for(var i=0;headers[i];i++){
// get the content of the current header
var content = headers[i].innerHTML;
// create a button, delete the content of the header
// replace it with the button and set the content to
// the cached header content
var a = document.createElement('button');
headers[i].innerHTML = '';
headers[i].appendChild(a);
a.innerHTML = content;
}
// apply a single click event to the menu
m.addEventListener('click',function(e){
// find the event target and chec that it was a button
var t = e.target;
if(t.nodeName.toLowerCase()==='button'){
// get the LI the button is in
var mom = t.parentNode.parentNode;
// check if its class name is not content and
// remove the open class if it exists, else
// add an open class
if(mom.className!=='current'){
if(mom.className === 'open'){
mom.className = '';
} else {
mom.className = 'open';
}
}
// don't do the normal things buttons do
e.preventDefault();
}
},true);
}
})()
This code also takes advantage of event delegation – there is so no need to apply and event handler to each heading – even if that is really easy to do with a library and an each() or batch() command. Event handling is a great concept and the tricks you find when you bother to read the docs are staggering.
Hiding is not removing
This is where most menu scripts stop. Great stuff, it expands and collapses and everybody is happy. Unless you are an unhappy chap and you need to use a keyboard to access it – or an older mobile device. Try it out yourself – use the second example by tabbing from link to link. You’ll find that you have to tab through the invisible links to reach the next section to open or hide. That surely can’t be the idea, right?
The trick to work around is to change the tabindex property of an element. A value of -1 will remove it from the tabbing order and setting it back to 0 will make them available again. So, to make keyboard access easier, let’s remove the tab order upfront and re-set or remove it when we show and hide the menus. This is terribly annoying as we have to loop through the links. I hate loops, but browsers are not our friends.
(function(){
// get the menu and make sure it exists
var m = document.getElementById('menu');
if(m){
// add a class to allow the CSS magic to happen
m.className = 'js';
// loop over all headers
var headers = m.getElementsByTagName('h3');
for(var i=0;headers[i];i++){
// get the content of the current header
var content = headers[i].innerHTML;
// create a button, delete the content of the header
// replace it with the button and set the content to
// the cached header content
var a = document.createElement('button');
headers[i].innerHTML = '';
headers[i].appendChild(a);
a.innerHTML = content;
}
// loop over all nested lists
// and set the taborder of the nested links to -1 to
// remove them from the tab order
var uls = m.getElementsByTagName('ul');
for(var i=0;uls[i];i++){
if(uls[i].parentNode.className!=='open' &&
uls[i].parentNode.className!=='current'){
var as = uls[i].getElementsByTagName('a');
tabOrder(as,-1);
}
}
// apply a single click event to the menu
m.addEventListener('click',function(e){
// find the event target and chec that it was a button
var t = e.target;
if(t.nodeName.toLowerCase()==='button'){
// get the LI the button is in
var mom = t.parentNode.parentNode;
// check if its class name is not content and
// remove the open class if it exists, else
// add an open class. Also, remove or add the
// links to the tab order
if(mom.className!=='current'){
if(mom.className === 'open'){
mom.className = '';
tabOrder(as,-1);
} else {
mom.className = 'open';
tabOrder(as,-1);
}
}
// don't do the normal things buttons do
e.preventDefault();
}
},true);
}
// remove from or add elements to the tab order
function tabOrder(elms,index){
for(var i=0;elms[i];i++){
elms[i].tabIndex = index;
}
}
})()
The third demo does exactly that – use your keyboard and marvel at being able to tab from parent element to parent element when sub-sections are collapsed.
Fixing it for Internet Explorer 6 and 7bad old browsers
Now, this is all good and fine but of course we need to fix the code to work around the quirks of browsers that don’t understand the W3C event model or consider an element with a name attribute the same as one with and id. We could fork and fix in the code we have here, but frankly I am tired of this. People give us libraries to work around browser differences and to make our lives easier. This is why we can use YUI for example to make this work reliably cross-browser:
<script type="text/javascript" charset="utf-8">
(function(){
var config = {
'id':'menu', // id of the menu
'jsenabled':'js', // class to add to apply design/functionality
'show':'open', // show the menu
'current':'current' // current item (will not be collapsed)
};
var YE = YAHOO.util.Event, YD = YAHOO.util.Dom, YS = YAHOO.util.Selector;
var m = YD.get(config.id);
if(m){
YD.addClass(m,config.jsenabled);
tabOrder(YS.query('#'+config.id+' ul a'),-1);
// set keyboard access for links in open sections - thanks David Lantner
tabOrder(YS.query('#'+config.id+' li.'+config.current+' a'),0);
tabOrder(YS.query('#'+config.id+' li.'+config.show+' a'),0);
YD.batch(YS.query('#'+config.id+' h3'),function(o){
var c = o.innerHTML;
var b = document.createElement('button');
o.innerHTML = '';
o.appendChild(b);
b.innerHTML = c;
});
YE.on(m,'click',function(e){
var t = YE.getTarget(e);
if(t.nodeName.toLowerCase()==='button'){
var mom = YD.getAncestorByTagName(t,'li');
if(!YD.hasClass(mom,config.current)){
var as = YS.query('ul a',mom);
if(YD.hasClass(mom,config.show)){
YD.removeClass(mom,config.show);
tabOrder(as,-1);
} else {
YD.addClass(mom,config.show);
tabOrder(as,0);
}
}
}
},true);
}
function tabOrder(elms,index){
for(var i=0;elms[i];i++){
elms[i].tabIndex = index;
}
}
})();
</script>
Good start
This is just a start to make this work. A real menu should also have support for all kind of keys that a menu like this in a real app gives us and notify screen readers of all the happenings. For this, we need ARIA. Right now, I’d be happy to get some comments here :)
Tags: accessibility, css, event delegation, javascript, keyboard access, menus, webdevtrick



June 23rd, 2009 at 11:39 pm
Twitter Comment
RT @codepo8 Ok, here are some basics on how to create a #js menu in a keyboard friendly and easily styleable manner: [link to post]
– Posted using Chat Catcher
June 23rd, 2009 at 11:42 pm
This is great.
The crazy nesting of divs and overuse of JS to make this stuff works always irked me. Now I have an article to point to when people ask how to create menus.
Thanks!
June 24th, 2009 at 12:03 am
Twitter Comment
Ok, here are some basics on how to create a JavaScript menu in a keyboard friendly and easily styleable manner: [link to post]
– Posted using Chat Catcher
June 24th, 2009 at 12:04 am
Twitter Comment
RT @codepo8: here are some basics on how to create a JavaScript menu in a keyboard friendly and easily styleable manner: [link to post]
– Posted using Chat Catcher
June 24th, 2009 at 1:19 am
Hi Chris,
it is really nice to see some ARIA without considering the complexity that is required by W3C specifications (WAI-ARIA). I am also adventuring in the development of ARIA, however my efforts are far behind yours (you can see it for yourself).
I would like to ask you about how do you express to the users of this widget that the H3
June 24th, 2009 at 2:43 am
Twitter Comment
Very nice Accessibility handling without the not so common and easy development WAI-ARIA especification [link to post]
– Posted using Chat Catcher
June 24th, 2009 at 3:04 am
Twitter Comment
Fancy accessible JavaScript menu hints via @codepo8 [link to post]
– Posted using Chat Catcher
June 24th, 2009 at 6:04 am
Twitter Comment
Oh,if everybody here in Austria would understand that article of @codepo8 we’d be a step in the future: [link to post] (todo: translate)
– Posted using Chat Catcher
June 24th, 2009 at 6:40 am
Twitter Comment
Wait till I come! » Blog Archive » Creating accessible JavaScript … [link to post]
– Posted using Chat Catcher
June 24th, 2009 at 7:47 am
Twitter Comment
Shared Post: Creating accessible JavaScript menu systems – some basic steps that get forgotten [link to post]
– Posted using Chat Catcher
June 24th, 2009 at 9:03 am
Twitter Comment
Oh,if everybody here in Austria would understand that article of @codepo8 we’d be a step in the future: [link to post] via @yatil
– Posted using Chat Catcher
June 24th, 2009 at 9:29 am
I have noticed that in some situations joining up an id selector and class selector like
“#menu.js” can have cross-browser issues.
As a personal preference I think I’d set tabIndex with setAttribute and remove it with removeAttribute to restore natural tab order.
June 24th, 2009 at 12:04 pm
Twitter Comment
“accessible JavaScript menu systems” [link to post]
– Posted using Chat Catcher
June 24th, 2009 at 2:45 pm
You’re in luck, Chris, because the WAI-ARIA 1.0 Working Draft contains an example which they call a “Tree Widget” but is the same:
http://www.w3.org/TR/wai-aria/#Exampletree
Note the interesting suggestion “to use CSS attribute selectors to toggle visibility or style of an item based on the value of an ARIA state or property” instead of assigning a class – but this is followed by the warning that this technique is not widely supported. Still, I look forward to a time when we can make a single DOM modification (performance hit) but gain style and grace (ARIA).
June 24th, 2009 at 2:46 pm
In your final script, line 13 assigns a tabindex value of “-1″ for all links but then line 25 excludes the links within the “current” section so they are not reachable since they still have a value of “-1″ – can the IF statement on line 25 just be removed?
June 24th, 2009 at 3:25 pm
Why use a `left: -9999px` when a `display:none` would work just as well?
I believe that it would also fix the tab index problem inherently as sections not displayed don’t get tab indexes.
June 24th, 2009 at 5:17 pm
@David Lantner, darnit. Great find, thank you. I altered the code to fix it.
@Rob yes and no. Display:none hides the content from screen readers but not from all.
June 25th, 2009 at 1:01 am
Chris, why do you use strict comparisons when comparing strings like HTMLElement#nodeName?
June 25th, 2009 at 2:35 pm
Subjectively `display:none` seems more correct to me, I suppose that I should just use both. Thanks for the info
I would categorize screen readers that read hidden content as buggy. I suppose it would be too much to ask that those screen readers would actually respect media aural or speech so I could hide the work around there.
June 25th, 2009 at 3:50 pm
Chris,
My question is about how do you express users of assistive technologies that the H3 elements in the widget can be clicked?
Thank you in advance for you attention.
Willian.
June 25th, 2009 at 4:46 pm
@Willian: As we inject buttons into the H3s that will happen automatically. If you turn off CSS you can even see them.
@Rob nothing stops you from doing that of course, I think there was some research on the topic that showed browsers do it wrong, too.
@Gabriel: Why not? Strict comparison is ingrained with me, I don’t even check if it is needed :)
July 9th, 2009 at 11:05 am
Twitter Comment
@jvwissen [link to post]
– Posted using Chat Catcher
July 9th, 2009 at 12:10 pm
Twitter Comment
@wnas hmm so the examples in that post should be accessible with keyboard???.. ( not here )
– Posted using Chat Catcher
July 9th, 2009 at 1:11 pm
Twitter Comment
@jvwissen try safari, am asking @codepo8 about it as we speak..
– Posted using Chat Catcher