12 votes

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');
  };
});

5 comments

  1. [2]
    KaiTheJedi
    Link
    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. Which part of the keyboard interaction is failing?...

    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.

    1. Which part of the keyboard interaction is failing? Tabbing, selecting with the arrow keys, or switching the active tab panel?
    2. Why do you want to avoid anchor tags?
    3. Have you considered using button tags instead? example. I recommend reverting to the original code from the page you linked and just replacing the anchor tags with button tags, if that’s an option for you. Then you can probably skip all the following points.
    4. Anchor and button tags (i.e. control elements) are tabbable by default, but list item tags are not. You must also add tabindex=0 to the default selected element in the HTML source.
    5. Similarly, adding tabindex=-1 to your list item elements is not necessary, because they are not tabbable to begin with. The caveat is that this only applies to your specific case because you’re setting tabindex=0 on the new selected tab before you call .focus(). If you were doing it in the reverse order you would still need the tabindex=-1.
    6. In some rare cases, screen readers can get mixed up if the DOM changes at the same time that .focus() is called. Adding a 100ms delay can help keep the screen reader output consistent.
    7. I’m not 100% sure you can just call the click() method directly like it is in the keydown method. I haven’t used jQuery in a while so maybe you can, but it might require you to supply the ev param, or override the onClick method instead. Does the browser console show any error messages?
    8. Sometimes older browsers, particularly Microsoft browsers since IE and sometimes Safari, have been more tolerant of incorrect or missing ARIA attributes by inferring the web author’s intentions to aid the user experience. This can cause inconsistent behaviour across browsers and may explain your observations from testing.
    9. Microsoft narrator is not a common or fully fledged screen reader. The standard testing screen readers are NVDA and JAWS on Windows, and VoiceOver on MacOS.
    7 votes
    1. dblohm7
      Link Parent
      Huge plus 1 to using NVDA on Windows.

      Huge plus 1 to using NVDA on Windows.

      3 votes
  2. smores
    Link
    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...

    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.

    3 votes
  3. first-must-burn
    Link
    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...

    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.

    2 votes
  4. gala_water
    Link
    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...

    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.