2009-02-07 jQuery Accessible Tabs - How to make tabs REALLY accessible
While a lot of Tabs-Scripts claim to be accessible it turns out, most of them are really not. While developing this jQuery Plugin with my coworker Artur Ortega, we tried to find a single existing Javascript powered Tabs-Interface that Artur, using his Screenreader, would actually be able to use properly. We eventually gave up the search.
The one Problem even the better ones have is the missing Feedback of the content change. Most of the tabs scripts change the style of the tabs and the visibility of their corresponding content but leave the user just where he was - on the tab-link - not knowing that something happened (just like clicking on a, ever so popular, inactive link (a href="#")).
This is also the big difference this Script makes. While the user experience for users without visual impairment is exactly the same, there's a lot happening under the hood. But let's start from the beginning.
jQuery Accessible Tabs uses a very simple and flexible HTML markup as it's base to build upon. All it needs it's a wrapper containing Headlines and content elements one after the other. This provides a nice non-javascript fallback that does not look like something is missing or broken.
<div class="tabs">
<h2>some dummy headline</h2>
<div class="tabbody">
<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit.</p>
<h3>Lorem ipsum</h3>
<p>Nullam malesuada suscipit pede. Nullam ipsum lacus, varius</p>
</div>
<h2>another dummy headline</h2>
<div class="tabbody">
<p>Integer tincidunt. Cras dapibus. Vivamus elementum nisi.</p>
<p>Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel.</p>
</div>
<h2>anything else</h2>
<div class="tabbody">
<p>Here could be your content</p>
</div>
</div>
A simple jQuery call then transforms this to the accessible Tabs:
$(document).ready(function(){
$('.tabs').accessibleTabs();
});
To not only be accessible but also to be as flexible as possible the script allows configurations to be passed to.
$('.tabs').accessibleTabs({
// Classname to apply to the div that is wrapped around
// the original Markup
wrapperClass: 'content',
// Classname to apply to the LI of the selected Tab
currentClass: 'current',
// Tag or valid Query Selector of the Elements to Transform the
// Tabs-Navigation from (originals are removed)
tabhead: 'h4',
// Tag or valid Query Selector of the Elements to be treated as
// the Tab Body
tabbody: '.tabbody',
// can be 'fadeIn', 'slideDown', 'show'
fx:'show',
// speed (String|Number): 'slow', 'normal', or 'fast') or the number of
// milliseconds to run the animation
fxspeed: 'normal',
// text to indicate for screenreaders which tab is the current one
currentInfoText: 'current tab: ',
// Definition where to insert the Info Text.
// Can be either 'prepend' or 'append'
currentInfoPosition: 'prepend',
// Class to apply to the span wrapping the CurrentInfoText
currentInfoClass: 'current-info'
});
So, why is this Tabs-Script more accessible than others?
The main thing this Script does better than the others is providing a click feedback for Screenreader Users. When the user clicks on one of the tabs there's actually a navigation going on on the page.
Every link in the tabs is pointing to an actually focusable content. To allow this, we create a named anchor inside of the tabs content. This anchor, as it has no href property, is not focusable by default though. To make it focusable we gave it a tabindex="0". Why 0? Because a tabindex of 0 does not change the natural tab flow of the page but it makes an element focusable which is not able to receive focus by default.
<h4>
<a id="accessibletabscontent" name="accessibletabscontent" class="accessibletabsanchor" tabindex="0">
some dummy headline
</a>
</h4>
Inside of the named anchor we add the same text as the tab that was just clicked and wrapped it into a headline element. This provides a very nice user experience for screenreader users, as it nicely confirms that you actually jumped into a new content of the page.
The other thing that adds benefit for screenreader users is a little text that indicates the currently selected tab. It defaults to "current tab: " followed by the default text of the tab. For internationalisation reasons this text as well as the text position (either before or after) can easily be configured.
Problem: Focussing hidden or recently hidden Elements
During the testing of the tabs we discovered a very interesting, yet very annoying, behaviour of the Screenreader Jaws (and probably others too). We wanted to focus the headline in the content of the current tab after clicking the tab which made the content visible. Everytime we tested, the Screenreader was unable to find the element to focus on and simply jumped to the end of the page or randomly somewhere else. We discovered that this is only happening when the focus target was recently hidden. This phenomenon is caused by the screenreaders virtual buffer, an internal copy of the actual DOM in which the Screenreader user navigates. The virtual buffer does regularly update itself from the Browser DOM but there's always a delay.
To work around this issue we used a different approach. The named anchor we're linking to is not actually in the hidden tab content but before it in the DOM of all of the contents and hidden offscreen. Also actually every link in the tabs is pointing to the same anchor. We're simply dynamically rewriting the headline text on the fly and show and hide the contents accordingly. The user experience is just the same. The screenreader user clicks on the tab, follows the link and finds the linked content starting with a corresponding headline.
You can see the script in action in in many different Examples here. This plugin is also part of the The CSS Framework Yaml
You can download the latest version of this script including the demos here.
Update:
Jason Kiss wrote this excellent followup about how to make this even better
His enhancements are now also in the official repository
You can always find the latest Code in the Accessible Tabs github repository
Comments
This is another useful step forward after the marvelous work you and Artur did on the Yahoo! Finance Currency Converter.
Just goes to show when you pair up a world class web developer who knows lots about accessibility with an experienced screen reader user who is an engineering guru, the combination of ideas and the end result are great examples of solving real accessibility problems.
Thanks Mike.
Coming from you this really means a lot
I completely Agree - Working with Artur clearly is a Web Developers dream.
I highly recommend it!
One small improvement would be to do some events delegation there:
$(el).find('ul li a').each(function(i){<br />
$(this).click(function(event){<br />
Event delegation can become nasty but at the end of the day, you like the flexibility it offers you: http://gist.github.com/29390 (btw, jQuery offers a lot of handy tools to make it cleaner than that).
PS: comment authoring could be better... btw, it's [/geshi] and not [/lang].
I usually use event delegation all the time. I totally shall implement it in the next release.
I fixed the bug in the geshi description text. Geshi really is a pain yet it seems to be the best code colouring plugin for this blog though...
Any way to have it autorotate through the tabs?
Thanks!
Aaron
at the moment there is no function to rotate the tabs. This newsticker script I did a few years ago might be what you are looking for: http://blog.ginader.de/dev/listnewsticker.html
They have just exactly the same Problem (staying on the Tab-Link after the click) than the others...
I am not an experienced JAWS user so perhaps I am missing something. I am running JAWS in default mode which I imagine most users do.
Would this be a barrier to someone or can we include hidden navigation info for screen reader users (use the down arrow key).
Any suggestions?
We're investigating this issue and try to work out an solution that does not need any extra handling.
My tab contents contain list of links. This conflicts with the following selector in jquery.tabs.js:
$(el).find('ul>li>a').each(function(i){
I solved it by adding a class 'links' on my own lists:
$(el).find('ul:not(.links)>li>a').each(function(i){
...but I find this unsatisfactory.
I propose that you add a class to the ul-tags in the tab list, so that the tabs-selector can be made more specific. This class could be made configurable through the defaults mechanism.
Hope this is of use.
Regards,
Wim.
I already been told about this issue and it's already fixed in my internal version and will be in the upcoming new release (together with a few very nice new features
Perhaps you are interested in seeing what I did with the Accessible Tabs: http://wimrijnders.nl/album/2009/oma_beppe/index.rb
I used them for selecting the year and displaying the subcategories per year in my photo album. They don't look like tabs at all but they fit my intention nicely.
Regards,
Wim.
These are great and I'm interested in using it. How would I go about have the changing the active via javascript in a function so that I change the active tab in response to an onclick event?
Thanks again.
This means that the syntax slightly changed: $('#container').tabs();
This also means that one can now use jQuery’s capability to find elements in the DOM tree (and need not to rely on an id) and also chain methods.
This is really interesting and i am trying to use this method for my website.
I have changed the javascript to find the last in the menu and apply class "last" to it.
Just wondering if anyone know if we could change the last so that it link to other page.
Thanks,
Bui
I tried to workaround
>> to add the class "last" into the last :
$(el).find("ul>li:last-child").addClass("last");
>> to change the url for the tag
$(el).find("ul>li:last-child").find("a[href]").attr('href','http://www.sailboatvn.com/');
but when I clicked on the last link.. it does not take user to anywhere ??
The "first" and "last" classes are a good idea though
It seems to me that the anchorlink works in the same way any anchorlink do - the browser scrolls to the anchor position. For a non-screenreader user, this is neither optimal, nor a standard way in how tabs work. Your tabs are really accessible, but to me, not quite optimal.
thanks for your Feedback. The main difference between the default anchor behavior and the technique used here is that setting the focus() to an element does not make this element jump to the top of the page (like an in-page-anchor annoyingly always does). It will only scroll the page when the focused element is currently not in the viewport. It will not change anything otherwise.
thanks for the important information.
It is superb, I really asked my friend for a similar tabs script but it seems more compact and efficient. Do you plan to add more features to it or any other updated release?
I grabbed the latest from github.com and I saw that you already implemented the function to select a tab via an index. Thus, I was able to retain the tab selection with a hidden field value by simply saving the index of the tab.
So, in my .aspx page I added this to keep track of the last selected tab during the last postback:
To show the selected tab I added this just below the declaration of the tabs:
$(".tabs").showAccessibleTab($("#hfSelectedTab").val());
In my jquery.tabs.js file I added this just below the $(this).click function:
$("#hfSelectedTab").val(i)
It works great but I did run into a error that I worked around. In the showAccessibleTab method, I made the following modification:
var links = el.find('ul.' + o + '>li>a'); //Modified from 'ul.' + o.options.tabsListClass + '>li>a'
Just thought I'd share this with you. It wasn't the cookie solution you proposed for the feature but my requirements were to solve this without the use of cookies.
Thanks for your work here!
It was a big help in getting our app 508 compliant.
I'm testing out your Accessible tabs plug-in and it works great so far. I just had a question. In the example code provided, all of the content for the various tabs are located in one html page. Is it possible to have multiple pages, so one page has content for one tab and the next page has content for another tab?
This would be useful because I'm working on a page with a lot of tab content and it would be easier to separate the tab content into separate pages.
Thanks!
thanks for the nice Feedback. I'm sorry but the feature you're talking about would be a big accessibility problem and therefore it is not and will never be implemented. If you need to/want to have the tab contents in separate files you should include them on the server side (i.e. SSI or PHP include) instead of in the client via ajax or frames.
It's actually very easy to use the tabs without any animation. You just need to define "show" as fx and set fxspeed to null like this:
$('.tabs').accessibleTabs({
fx:'show',
fxspeed:null
});
Happy you like it
One other note, if I include an unordered list in the tabbody then the plugin takes this for the navigation. I changed it to have a unique ID by editing your plugin code for the time being, but since you have an option to customise everything maybe this is a good idea as well.
I used this plugin and it works great.
Recently, i have spotted an issue with Safari Browser, when user clicks on one of the tab, its content flashing for a second and disappear.
Please take a look at the demo here:
http://www.quaycreative.com/portfolio/
Anyone know what is happening?
Thanks
I added your idea as new feature request to github:
http://github.com/ginader/Accessible-Tabs/issues/issue/12
excellent information, thank you thanks for the inspiration!
I think it may be possible by altering the script by replacing ul>li:first with ul>li:nth-child('+randomTab').
you can use the existing method "showAccessibleTab(n)" to achieve that.
I.e.:
$(document).ready(function(){
var tabs = $(".tabs").accessibleTabs();
var amountOfTabs = 3;
var randomTab = Math.floor(Math.random()*amountOfTabs);
tabs.showAccessibleTab(randomTab);
});
thanks for your time ,
i helped me in aking my own tabs too,:-)
Thanks a lot
in my simple example here: http://blog.ginader.de/dev/jquery/tabs/1.0/simple.php I'm using the class ".accessibletabsanchor" that is attached to the anchor in the headline to move it offscreen like so:
.js .tabs .current-info,
.js .tabs .accessibletabsanchor {
left:-999em;
position:absolute;
}
.tabs .content h2 {
display:none;
}
or not? This CSS rule would apply only with javascript active so it's similar to:
.js .tabs .accessibletabsanchor {
left:-999em;
position:absolute;
}
What do you think?
Thanks
It's very important to not use display:none on this headline as everything you hide using display:none or visibility:hidden is then also hidden to Screenreader users. For those exactly I do add this though so the anchor in the headline can be used as link target after clicking on the tab. If the gap causes problems in your design you can also change the css to:
.js .tabs .current-info,
.js .tabs h2{
position:absolute;
left:-999em;
}
By moving the h2 offscreen instead of the anchor the gap should be gone.
Is there any way to link directly to a specific tab?
Thanks,
Rob
I just released the shiny new version 1.6: http://mz05.de/1w which introduces (beside other things) the new feature "autoAnchor". If you set this to true in your config and add IDs to the headlines in your tabs markup these IDs can then be used to directly link to the tab!
Flash Designers UK
Is there any way you could provide an example as to how I would go about setting up the saveState feature?
Thanks
Rob
http://blog.ginader.de/dev/jquery/tabs/1.7/save-state.php
You can find many more Examples (and short Explanations at the Bottom of the Examples) here: http://blog.ginader.de/dev/jquery/tabs/1.7/index.php
Please let me know if anything is hard to understand. I want this to be self explanatory
Thanks
when the tabs are outside of the viewport (also partially) then then Browser will try to move them into view. This happens automatically after the selected tab content gets focused. This is an important accessibility feature and should not be avoided.
english