jQuery UI Accordion keyboard accessibility tests
Last updated: April 27, 2009
If you find any of the information useful, or if you find anything to be incomplete or inaccurate, please let me know.
Summary
This test page explores keyboard accessibility problems with accordion interfaces, looking specifically at jQuery UI’s Accordion interface widget. The supporting article for this test page can be found on my blog: jQuery UI accordion: enabling keyboard navigation.
Introduction
The jQuery UI Accordion plugin applies tabindex="0" to the element acting as the Accordion header. This is actually done for accessibility reasons; it should tell the browser to make the header element part of the tab order, making it more accessible by keyboard. However, it causes a problem in Firefox, where the Accordion cannot be navigated using the keyboard. The jQuery UI team are aware of this problem and intend to remove the tabindex="0" set by the Accordion code.
To ensure accessibility, the Accordion header should be a normal focusable element, e.g. a hyperlink. The onclick event handlers on such elements are sure to behave correctly under normal usage, so they can be used to trigger the Accordion behaviour. Unfortunately, the Accordion will currently not work correctly if the Accordion header is wrapped in an outer element, e.g. <h2><a class="ui-accordion-header"></a></h2>.
Below are some example implementations to demonstrate the broken keyboard accessibility and some suggested solutions. Notes are included in the first section of each example, followed by the jQuery code and markup used.
NB: If the Accordion does not load correctly, it's likely that the jQuery files have not loaded. If this happens, either wait for a moment to give the files a chance to load, or try refreshing this page.
Tests
-
Accordion 1 notes
Control (jQuery UI sets tabindex="0"): not accessible.
Due to a tabindex bug in Firefox, you can tab to each accordion header, but in this example, the element does not allow activation via keyboard, not even via the onclick event; you must use a link or a button for this purpose. Furthermore, the bug does not permit a user to tab to the link contained within the accordion header. You cannot tab backwards through the headers.
-
Accordion 1 jQuery
// Control (jQuery UI sets tabindex="0"). console.info("Control: #accordion1 .ui-accordion-header"); $("#accordion1") .accordion({ header: ".ui-accordion-header" }); $("#accordion1 .ui-accordion-header") .each(function(){console.log(this)}); -
Accordion 1 markup
<ul id="accordion{N}"> <li class="ui-accordion-group"> <h3 class="ui-accordion-header"><a href="#">Accordion {N} notes</a></h3> <div class="ui-accordion-content"> <h4>{TITLE}</h4> {NOTES} </div> </li> <li class="ui-accordion-group"> <h3 class="ui-accordion-header"><a href="#">Accordion {N} jQuery</a></h3> <div class="ui-accordion-content"> <pre class="code"><code>{JQUERY}</code></pre> </div> </li> <li class="ui-accordion-group"> <h3 class="ui-accordion-header"><a href="#">Accordion {N} markup</a></h3> <div class="ui-accordion-content"> <pre class="code"><code>{MARKUP}</code></pre> </div> </li> </ul>
-
Accordion 2 notes
The "undo" solution: a blank tabindex (tabindex="").
This solution removes the tabindex attribute set by jQuery UI using this code:
$(".ui-accordion-header")
.attr("tabindex",""); -
Accordion 2 jQuery
// Blank tabindex (tabindex=""). console.info("Undo: #accordion2 .ui-accordion-header"); $("#accordion2") .accordion({ header: ".ui-accordion-header" }); $("#accordion2 .ui-accordion-header") .each(function(){console.log(this)}) .attr("tabindex",""); console.log("tabindex attributes blanked:"); $("#accordion2 .ui-accordion-header") .each(function(){console.log(this)}); -
Accordion 2 markup
<ul id="accordion{N}"> <li class="ui-accordion-group"> <h3 class="ui-accordion-header"><a href="#">Accordion {N} notes</a></h3> <div class="ui-accordion-content"> <h4>{TITLE}</h4> {NOTES} </div> </li> <li class="ui-accordion-group"> <h3 class="ui-accordion-header"><a href="#">Accordion {N} jQuery</a></h3> <div class="ui-accordion-content"> <pre class="code"><code>{JQUERY}</code></pre> </div> </li> <li class="ui-accordion-group"> <h3 class="ui-accordion-header"><a href="#">Accordion {N} markup</a></h3> <div class="ui-accordion-content"> <pre class="code"><code>{MARKUP}</code></pre> </div> </li> </ul>
-
Accordion 3 notes
The "redo" solution: using tabindex="-1".
This solution counteracts the tabindex attribute set by jQuery UI using this code:
$(".ui-accordion-header")
.attr("tabindex",-1); -
Accordion 3 jQuery
// Pseudo-focusable tabindex (tabindex="-1"). console.info("Redo: #accordion3 .ui-accordion-header"); $("#accordion3") .accordion({ header: ".ui-accordion-header" }); $("#accordion3 .ui-accordion-header") .each(function(){console.log(this)}) .attr("tabindex",-1); console.log("tabindex attributes set to -1:"); $("#accordion3 .ui-accordion-header") .each(function(){console.log(this)}); -
Accordion 3 markup
<ul id="accordion{N}"> <li class="ui-accordion-group"> <h3 class="ui-accordion-header"><a href="#">Accordion {N} notes</a></h3> <div class="ui-accordion-content"> <h4>{TITLE}</h4> {NOTES} </div> </li> <li class="ui-accordion-group"> <h3 class="ui-accordion-header"><a href="#">Accordion {N} jQuery</a></h3> <div class="ui-accordion-content"> <pre class="code"><code>{JQUERY}</code></pre> </div> </li> <li class="ui-accordion-group"> <h3 class="ui-accordion-header"><a href="#">Accordion {N} markup</a></h3> <div class="ui-accordion-content"> <pre class="code"><code>{MARKUP}</code></pre> </div> </li> </ul>
-
Accordion 4 notes
A slightly better approach: use a proper element for the accordion header.
Use an actionable element as the accordion header in the first place and style it appropriately.
If it doesn't make sense to have the accordion header links in your markup, you could dynamically add the links to this example with JavaScript.
-
Accordion 4 jQuery
// Slightly better approach useing an actionable element as the accordion header. console.info("Using anchors: #accordion4 .ui-accordion-header"); $("#accordion4") .accordion({ header: ".ui-accordion-header" }); $("#accordion4 .ui-accordion-header") .each(function(){console.log(this)}); -
Accordion 4 markup
<ul id="accordion{N}"> <li class="ui-accordion-group"> <a href="#" class="ui-accordion-header">Accordion {N} notes</a> <div class="ui-accordion-content"> <h4>{TITLE}</h4> {NOTES} </div> </li> <li class="ui-accordion-group"> <a href="#" class="ui-accordion-header">Accordion {N} jQuery</a> <div class="ui-accordion-content"> <pre class="code"><code>{JQUERY}</code></pre> </div> </li> <li class="ui-accordion-group"> <a href="#" class="ui-accordion-header">Accordion {N} markup</a> <div class="ui-accordion-content"> <pre class="code"><code>{MARKUP}</code></pre> </div> </li> </ul>
-
Accordion 5 notes
A progressive approach: dynamically add accordion headers.
It makes sense to include headings for each accordion group. The problem with this is that you can end up with the same problem as before. The accordion will not work if we wrap the link acting as our accordion header inside another element; <h3><a class=".ui-accordion-header"> will not work. If we want to do this in our markup, we can dynamically add a link with JavaScript and use the tabindex fix as before.
Again, use an actionable element as the accordion header in the first place and style it appropriately, but this time start with clean markup and progressively enhance.
-
Accordion 5 jQuery
// Dynamically add accordion headers. console.info("Dynamically added anchors: #accordion5 .ui-accordion-header"); $("#accordion5 li") .addClass("ui-accordion-group"); $("#accordion5 .li-a h3:first-child") .addClass("ui-accordion-header") .wrapInner("<a href=\"#\"><\/a>") .each(function() { $(this).siblings().wrapAll("<div class=\"ui-accordion-content\"><\/div>"); }); $("#accordion5 .li-a .ui-accordion-content").ready(function(){ $("#accordion5 .li-b h3:first-child") .addClass("ui-accordion-header") .wrapInner("<a href=\"#\"><\/a>") .each(function() { $(this).siblings().wrapAll("<div class=\"ui-accordion-content\"><\/div>"); }); }); $("#accordion5 .li-b .ui-accordion-content").ready(function(){ $("#accordion5 .li-c h3:first-child") .addClass("ui-accordion-header") .wrapInner("<a href=\"#\"><\/a>") .each(function() { $(this).siblings().wrapAll("<div class=\"ui-accordion-content\"><\/div>"); }); }); $("#accordion5 .li-c .ui-accordion-content").ready(function(){ $("#accordion5") .accordion({ header: ".ui-accordion-header" }); }); $("#accordion5 .ui-accordion-header") .attr("tabindex","") .each(function(){console.log(this)}); -
Accordion 5 markup
<ul id="accordion{N}"> <li class="li-a"> <h3>Accordion {N} notes</h3> <h4>{TITLE}</h4> {NOTES} </li> <li class="li-b"> <h3>Accordion {N} jQuery</h3> <pre class="code"><code>{JQUERY}</code></pre> </li> <li class="li-c"> <h3>Accordion {N} markup</h3> <pre class="code"><code>{MARKUP}</code></pre> </li> </ul>