Jump to main content

Trouble Tabbing To Tabs and Cheeky Checking Checkboxes

  • Published:
  • Updated:
  • Category: bugs, accessibility

Sometimes, despite best efforts, there will be accessibility issues in a code base.

Sometimes, those issues can create an awkward or annoying user experience.

And sometimes, those issues aren’t a result of a developer or their team doing anything incorrectly.

This is a tale of two bugs. The first concerns Tab Widgets, the second Checkboxes.

The tab issue

Imagine there’s a Tab Widget containing three tabs within the tablist.

Their accessible names are “Apples”, “Bananas”, and “Oranges” (naming things is hard).

Now, let’s say that “Bananas”, the middle tab, is currently active. Bananas could be active due to it being set as the default tab (for some reason), or more likely, because a user was navigating the component and activated it. Regardless, it would likely have markup resembling the following:

<h3>Learn about fruit or something I guess!</h3>

<div role="tablist">
  <span role="tab" tabindex="-1">Apples</span>
  <span role="tab" tabindex="0">Bananas</span>
  <span role="tab" tabindex="-1">Oranges</span>
  Tab panels would then go here. 
  They'd be filled with fruity knowledge! 

Great, our scenario is setup! I hope you are enjoying this journey so far.

Now, let’s imagine there’s a user navigating this document using either JAWS or NVDA with their best-paired browser of choice. If they were navigating the document with the virtual cursor (Down arrow key), or by heading elements, they would eventually reach the heading prior to the tablist. At this point, if they hit the Tab key to enter the tablist, where would you expect keyboard focus to land?

If you guessed focus would go to the tab with tabindex="0", well, obvious expectations don’t always match reality. Also, as noted, this is a blog post about bugs…

Testing and results

Various different implementations of Tab Widgets were tested using NVDA 2018.3.2 and JAWS 18 & 2018 (all using default settings). Additionally a separate reduced test case (opens in new window) was also tested to remove any possibility that a rogue line of JavaScript was the culprit here (which seemed unlikely since the different Tab Widgets all had the same issue… but due diligence n’ all).

For each instance, if navigating by the virtual cursor and then, prior to entering a tablist, hitting the Tab key, keyboard focus would always be set to “Apples” (which had a -1 tabindex) rather than the expected active tab, “Bananas”.

If navigating similarly but in reverse DOM order, as someone might do to return to the tablist from an exposed tabpanel, the focused tab would be “Oranges”, regardless of tabindex value.

Regardless of the tabindex value, “Oranges” would be focused tab if the user is navigating in reverse DOM order. Many users do this to return from an exposed tabpanel to its associated tablist.

This is not expected

Just to level set here, this is not expected behavior. If a screen reader were not running, a tab with a -1 tabindex would not receive keyboard focus.

Similarly, if navigating with JAWS or NVDA and using the Tab key alone, keyboard focus correctly goes to the active tab, skipping over any tabs with tabindex="-1", as long as keyboard focus had first encountered another focusable element prior to the tablist. For comparison purposes, this issue does not exist when using VoiceOver with either Safari or Chrome on macOS High Sierra.

Issues have been filed on the open bug tracker for JAWS and for the NVDA screen reader.

tabs, check. Checkboxes…

Unlike the tab focus issue, the checkbox issue is much more straight forward. Its isolated to Safari 12 and VoiceOver, and requires a specific markup pattern.

For example:

<input type="checkbox" id="a">
<label for="a">
  I agree to something

<!-- or -->

<label for="b">
  <input type="checkbox" id="b"> 
  I agree to something

With VoiceOver on, navigating to a checkbox built using the above patterns, using either VO key + left or right arrows, or the Tab key, VoiceOver will announce the accessible name of the checkbox, twice.

“I agree to something, I agree to something, unchecked, checkbox.”

If using a markup pattern such as:

<label for="c">
  I agree to something
<input type="checkbox" id="c">

You’d hear an expected announcement:

“I agree to something, unchecked, checkbox.”

Here’s a checkbox test page to try it out for yourself.

Two bugs, diverging paths of resolution

Bugs happen, and the best thing to do in such situations is to report them to the appropriate channels. Being that both of these bugs are not due to any flagrant coding error, one could take the approach that “it’s not my bug, it’s not my problem.”

And you know what, I respect that. That’s honestly how I’m looking at the checkbox bug. It’s definitely annoying, but any work around to get the appropriate announcement would likely have to involve some sort of extra markup, CSS reordering of the elements, just general nonsense that would have to be remembered and repeated with each new implementation of a checkbox to fix this one browser + screen reader pairing.

But then, there’s the tab issue. This issue appears to be a bug with the screen readers, so it’s not isolated to a single browser. It’s not an issue that necessarily breaks accessibility, but again, it can be annoying and does result in unexpected behavior.

Also, unlike the checkbox issue, this is mostly fixable by adding a relatively simple helper function to a Tab widget’s script.

var checkYoSelf = function ( index ) {
  if ( index !== activeIndex ) {

Within my Tab Widget script, I keep track of the number of tabs (index) as well as the currently active tab (activeIndex). Adding a focus event listener to each tab, the above function runs and performs a check to see if the currently focused tab is also the active tab. If not, the focusActiveTab function fires, and focus immediately is moved to the correct tab in the tablist.

While this produces the correct behavior and announcement with NVDA, JAWS will now correctly focus the appropriate Tab but will make no immediate announcement after doing so. This is not ideal, but is better than the behavior of announcing an inactive tab, saying that to activate it to “press space bar”, but nothing actually occurring when doing so.

Needless to say, this is a stop gap which can help fix experiences for NVDA users, but a true fix needs to be made to address all user experiences.

Parting words

Regardless of where a bug comes from, it should be our goal to provide the best user experience we can to all people interacting with what we’ve built. But we should also be realistic about where time is best spent in resolving bugs.

If it’s your code that caused a bug… well, time to get crackin’!

If it’s a bug due to a browser or screen reader, open issues and then make a judgment call for how to handle it in the meantime.

Would it require some considerable hacks and future tech debt to take care of? Is it a bug that creates more of an annoyance, rather than a truly broken user experience? Well, that might not be something you or your team can (or should) take on.

But, if you can spend a bit of time crafting a workaround that won’t balloon development time or create a new bug when the actual bug is fixed, then why not? It’s always cool to help people whenever you can!

Filing Bugs

Thanks are due

I wanted to give a quick shout out to Léonie Watson for helping me uncover the Tab widget bug, and providing excellent feedback and insight into the UX of Tab Widgets in general (more on that later). And to Eric Bailey for providing (yet another) late night proof read. You two are ace.