Help with web accessibility problem for screen readers - ARIA
I'm attempting to make my company's online software documentation ADA-accessible. We have several JavaScript elements that are currently not accessible to a screen reader. The one I'm working on is a set of tabs that show/hide content depending on which one you click. I am trying to use ARIA tags to make the tabs accessible. I have based my code off of this page, with some modifications. Namely, they use <a>
tags as the active element and I do not want to use <a>
tags.
While I'm able to get the tabs to work fine for me (a sighted person), testing with a screen reader shows mixed results. I can get the tabs to work using the Windows Narrator screen reader on Microsoft Edge in Windows 10, but not Chrome or Firefox in Windows 11. In those configurations, it is impossible to switch tabs. (I haven't tested every possible permutation, but it's probably a browser issue.) I don't know why this is happening because I have set up all my ARIA tags, and it does work on Edge.
Can anyone help me understand what I'm doing wrong so I can debug the issue and improve the code? Requirements are:
- The first tab must be selected (showing content) by default. Other tab content must be hidden by default, except the tab button to click on
- Sighted users must be able to navigate between tabs by clicking on the tab headers via mouse
- Visually impaired users must be able to navigate between tabs via keyboard/screen reader
- The presence, function, and usage of the tabs must be clear to the screen reader
- I do not want to use an
<a>
tag nested in the<li>
as the active element because this causes the page to jump around when sighted users click on it. I would like the entire "button" (right now, the<li>
tag) to be clickable. - Must work on all/most common browsers and popular operating systems
I suspect this is a JS issue, but I'm at a loss here and I don't know how to proceed.
Click to view HTML
<ul class="tabs-list" role="tablist">
<li class="tab current" aria-controls="example-1" aria-selected="true" href="#example-1" id="tab-example-1" role="tab">Example 1</li>
<li class="tab" aria-controls="example-2" aria-selected="false" href="#example-2" id="tab-example-2" role="tab">Example 2</li>
<li class="tab" aria-controls="example-3" aria-selected="false" href="#example-3" id="tab-example-3" role="tab">Example 3</li>
</ul>
<div aria-labelledby="tab-example-1" class="tab-panel current" id="example-1" role="tabpanel">
<p>Example 1 content goes here</p>
</div>
<div aria-labelledby="tab-example-2" class="tab-panel hidden" id="example-2" role="tabpanel">
<p>Example 2 content goes here</p>
</div>
<div aria-labelledby="tab-example-3" class="tab-panel hidden" id="example-3" role="tabpanel">
<p>Example 3 content goes here</p>
</div>
Click to view CSS
ul.tabs-list {
margin-left: 2px;
margin-right: 2px;
padding: 0px;
list-style: none;
position: relative;
line-height: 8pt;
}
ul.tabs-list:after
{
position: absolute;
content: "";
width: 100%;
bottom: 0;
left: 0;
border-bottom: 1px solid #ddd;
}
ul.tabs-list li {
color: #333;
display: inline-block;
padding: 10px 10px;
cursor: pointer;
position: relative;
z-index: 0;
}
ul.tabs-list li.current {
background: #fff;
color: #d9232e;
border-top: 1px solid #ddd;
border-bottom: 0px solid white;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
z-index: 100;
}
ul.tabs-list li:hover {
background: #c2c2c2;
color: #000;
display: inline-block;
padding: 10px 10px;
cursor: pointer;
border: 1px solid transparent;
}
ul.tabs-list li.current:hover {
background: #fff;
color: #d9232e;
margin-left: 0px;
border-top: 1px solid #ddd;
border-bottom: 1px solid transparent;
border-left: 1px solid #ddd;
border-right: 1px solid #ddd;
z-index: 2;
}
div.tab-panel {
display: none;
background: #ededed;
padding: 15px;
background-color: transparent;
}
div.tab-panel.current {
display: inherit;
}
Click to view JavaScript
$(function(){
var index = 0;
var $tabs = $('li.tab');
$tabs.bind(
{
// on keydown,
// determine which tab to select
keydown: function(ev){
var LEFT_ARROW = 37;
var UP_ARROW = 38;
var RIGHT_ARROW = 39;
var DOWN_ARROW = 40;
var k = ev.which || ev.keyCode;
// if the key pressed was an arrow key
if (k >= LEFT_ARROW && k <= DOWN_ARROW){
// move left one tab for left and up arrows
if (k == LEFT_ARROW || k == UP_ARROW){
if (index > 0) {
index--;
}
// unless you are on the first tab,
// in which case select the last tab.
else {
index = $tabs.length - 1;
}
}
// move right one tab for right and down arrows
else if (k == RIGHT_ARROW || k == DOWN_ARROW){
if (index < ($tabs.length - 1)){
index++;
}
// unless you're at the last tab,
// in which case select the first one
else {
index = 0;
}
}
// trigger a click event on the tab to move to
$($tabs.get(index)).click();
ev.preventDefault();
}
},
// just make the clicked tab the selected one
click: function(ev){
index = $.inArray(this, $tabs.get());
setFocus();
ev.preventDefault();
}
});
var setFocus = function(){
// undo tab control selected state,
// and make them not selectable with the tab key
// (all tabs)
$tabs.attr(
{
tabindex: '-1',
'aria-selected': 'false'
});
// hide all tab panels.
$('.tab-panel').removeClass('current');
// make the selected tab the selected one, shift focus to it
$($tabs.get(index)).attr(
{
tabindex: '0',
'aria-selected': 'true'
}).focus();
// handle <li> current class (for coloring the tabs)
$($tabs.get(index)).siblings().removeClass('current');
$($tabs.get(index)).addClass('current');
// add a current class also to the tab panel
// controlled by the clicked tab
$("#"+$($tabs.get(index)).attr('aria-controls')).addClass('current');
};
});
Hello, please forgive my lack of code formatting. I’m on mobile and can’t find the backtick key on iOS keyboard. TLDR; use the button tag.
Huge plus 1 to using NVDA on Windows.
Huge +1 to @KaiTheJedi's recommendation to use a button. If you want an action to occur when a user clicks something, then you should use a button!
To add more to that:
Whenever implementing a common pattern or widget, I highly recommend relying on the official WAI ARIA patterns and examples. In this case, for a tabbed panel interface, you want to look at the "Tabs" pattern, as well as the example implementation for tabs with manual activation: https://www.w3.org/WAI/ARIA/apg/patterns/tabs/examples/tabs-manual/. There's a codepen linked in the example, and you'll notice that they use buttons for the
role="tab"
element.Have you considered using a library like Bootstrap? They have all kinds of reactive components, including tabs, and there is guidance in their documentation on accessibility. I suspect their cross-browser support would be better and you would not be reinventing the wheel.
At the very least, you can quickly mock up a page with bootstrap tabs and see if it solves your problem.
There are, of course, many other similar libraries, Bootstrap is just the one I used for my quotation site.
Thank you everyone for taking the time to respond. I have reworked my code based on your feedback and on some examples I found on the "a11y" website recommended to me earlier, all ideas more elegant than what I was doing.
I realized that the way I was using the anchor tags before was the issue and not anchor tags in general. I am still using
<li>
elements but with<a>
tags inside. I have a lot more JS than I did before, but the HTML is much lighter, which is more important for my team because not all of our writers who would be interacting with the HTML are super technical or familiar with ARIA. I have kept it close to the a11y version with only minor modifications so that it's easier for me to maintain.It seems that we have gotten it more or less working. I am very glad that our documentation can be fully accessible now. I can only imagine how difficult it is for non-sighted people to use web resources and I am pleased that we can make that less frustrating. The next step will be to do some user testing to smooth out the edge cases.