Should selection follow focus in a tab interface?
I was recently improving the keyboard accessibility around our “tabs” component in our design system. It turns out that the method we were using wasn’t right, so I went down a path of learning the correct way to mark up a tabs style component with the correct WAI-ARIA roles.
The notion of tabs is an interesting one. When it comes to accessibility on the web, and especially with regards to ensuring users can access the information they need on a page, it is fairly well known that designers and developers should avoid “hiding” information for the sake of aesthetics. A common example of this is showing specific information only on hover, which—unless also revealed on keyboard focus—bars some users from accessing that information if they don’t use a mouse or if they are on a touch-screen device. Tabs are often used to categorise content into roughly the same sized area on one page. You click a tab and the content beneath the tab changes to reflect the content related to the tab you just clicked.
This post is not about agreeing or disagreeing with tabbed content, but I believe it should be used sparingly. I think it makes sense when filtering through groups of data that are related, but I think that a poor example of tab use is on e-commerce websites that separate the “product details”, “product care”, and “shipping details” into individual tabs rather than displaying all that critical information together, and making it always visible. Tabs are cool, but they have their time and place.
Are radio inputs for tabs actually a hack?
In the history of front-end and UI development, it became easy to rely on JavaScript or jQuery to handle specific interactions, and the preferred method was to find a solution with only CSS. Somewhere along the line, we found a seemingly perfect way to do tabs: radio inputs.
As you cycle through a regular group of radio inputs with your keyboard, each item you’re focused on is also selected. Understanding how a form works, radio inputs are used where one option in the group must be chosen. Using this, it’s a common “hack” to use the input as a means of selecting an active tab out of a tab group and displaying its contents. You selected or were focused on a radio input, and specific content displayed on the page until you selected a different option in the same radio input group. You hid the radio inputs visually by using the clip method, and called it a day.
A quick internet search shows me a slew of articles and tutorials demonstrating that the label
element and an input
with type="radio"
can get you “CSS-only tabs” that are “keyboard accessible”. I understand this. I know this. This is a simplistic representation of how ours used to be built (consider that we are using React):
<ul role="tablist"> <li role="presentation"> <input id="tab-apples" type="radio" role="tab" aria-controls="tab-content-apples" aria-selected="true" /> <label for="tab-apples" role="presentation">Apples</label> </li> <li role="presentation"> <input id="tab-oranges" type="radio" role="tab" aria-controls="tab-content-oranges" aria-selected="false" /> <label for="tab-oranges" role="presentation">Oranges</label> </li> <li role="presentation"> <input id="tab-grapes" type="radio" role="tab" aria-controls="tab-content-grapes" aria-selected="false" /> <label for="tab-grapes" role="presentation">Grapes</label> </li> </ul> <div role="tabpanel" id="tab-content-apples" aria-labelledby="tab-apples"> Content here </div>
It looks like we’re trying to make these tabs accessible, from looking at just the code itself. But stripping back some of the attributes and looking just at the tabs, this is what we’re looking at:
<ul role="tablist"> <li role="presentation"> <input type="radio" role="tab" /> <label role="presentation">Apples</label> </li> <li role="presentation"> <input type="radio" role="tab" /> <label role="presentation">Oranges</label> </li> <li role="presentation"> <input type="radio" role="tab" /> <label role="presentation">Grapes</label> </li> </ul>
It’s a bit strange. It looks a little wrong. Why are we slapping role="presentation"
on so many things? And on a label
, no less?
Achieving keyboard accessibility and being able to navigate tabs with a keyboard is great. But it could be improved by using the correct markup and ARIA roles—which allow assistive technologies to support interaction for pieces of user interfaces that are consistent with users’ expectations of those interfaces. To give an example, if a screen reader is aware that tabs and menus are present on the page, then it can provide the right options or shortcuts for someone using the screen reader to navigate those tabs and menus the way one might expect to move through a series of tabs—left and right, usually—or a group of items in a menu—up and down and maybe even using letter keys to skip to certain menu items.
As I write this, it awkwardly obvious that an input
is not the best choice for a tab. But it wasn’t until the Lighthouse tool in Google Chrome flagged an invalid ARIA role that we realised where we went wrong. (I also want to point out that Lighthouse is merely a tool, and we were only using it to review things on a surface level.) role="tab"
cannot be used on the input
element. Not only does it seem strange for an input to have a role of a tab, but I learned that <input type="radio">
implicitly has a role of radio
already. So assigning a different role to it makes no sense.
Attempting to add semantics might add complexity, and not add benefit
I remember when HTML5 elements were on the rise. We loved to use them, we wanted to collect them all. We would flex at being able to use article
and section
and summary
on a single page. In this vein, our team collectively decided that the “most semantic” way to mark up a group of tabs was in an unordered list—a ul
element. It was a list of tabs, duh. However, this feels like a case of semantics for the sake of semantics, which can have an adverse effect.
In attempting to fix the markup of what we’d already done, I thought it would be fine enough if I replaced our input
elements with button
, but Lighthouse still flagged the buttons in a list as a problem:
<ul role="tablist"> <li><button role="tab">Apples</button></li> <li><button role="tab">Oranges</button></li> <li><button role="tab">Grapes</button></li> </ul>
There wasn’t any additional benefit of using a list structure for a group of tabs. It’s actually unnecessary. 🫣 It turns out that when using role="tablist"
, the direct children of those are expected to be role="tab"
, and those tabs shouldn’t be placed inside of any other element. Keep it simple, as they say. This is what we went with in the end, and obviously had to move our CSS around to make sure things remained the same visually.
<div role="tablist"> <button role="tab">Tab 1</button> <button role="tab">Tab 2</button> <button role="tab">Tab 3</button> </div>
The role of roles
In doing research for this, I also learned that role="tab"
does not support semantic child elements, which means that all descendants of an element with that role will be role="presentation"
by default. For example, if we used a <div role="tab">
, no heading element, button
or other semantic element would actually be represented. In the following example:
<div role="presentation"><h3>Cool beans</h3></div>
Is actually understood as:
<div role="presentation">Cool beans</div>
Our resulting code looks a little like the code below. For the tabindex, in React we use a property isCurrentTab
and use tabindex={isCurrentTab ? undefined : -1}
. The importance of applying -1
is so that the inactive tabs are taken out of the keyboard Tab
key sequence and the user can navigate to the next element after the tab list.
<div role="tablist"> <button role="tab" id="tab-apples" aria-controls="tab-content-apples" aria-selected="true">Apples</button> <button role="tab" id="tab-oranges" aria-controls="tab-content-oranges" aria-selected="false" tabindex="-1">Oranges</button> <button role="tab" id="tab-grapes" aria-controls="tab-content-grapes" aria-selected="false" tabindex="-1">Grapes</button> </div> <div role="tabpanel" id="tab-content-apples" aria-labelledby="tab-apples"> Content here </div>
As I was updating the code I accidentally renamed the id
of the tabs, which I forgot was tied to the aria-labelledby
of each individual tab panel. When I made that mistake, I learned that if the two values don’t match, a screen reader won’t announce that you’re in a tab panel if you’re focused on something inside of it. But that is obviously important for users to know.
Now that that’s out of the way, and tested with a screen reader, and it’s communicating exactly what we want… there was another thought on my mind.
Should selection follow focus?
The more I test cases where a user might be using a screen reader or assistive technologies, the more I learn about accessibility and the importance of a website’s experience to someone using those technologies. Having a page marked up correctly, landmarks identified, and appropriate and clear visual cues to a user’s present location on the page are all contributors to a pleasant experience for users. Before our improvements, the behaviour of our tabs component meant that if you focused on the current tab and used your arrow keys to navigate, the action would not only focus on adjacent tabs, but would select the tab as well. I later learned that this behaviour is outlined in the ARIA APG (Authoring Practices Guide) as making selection follow focus, in a section about developing a keyboard interface.
This behaviour is fine, especially if tabs are marked up properly and users can identify that they are in a tab group. But before I even came across the APG I began to think about the possibility of allowing the user to explicitly choose the tab by being able to focus on it, then use another key to actually select it.
Pre-selecting for the user vs. letting the user make an explicit choice
Historically, our user feedback and research has suggested that “extra clicks” are a point of friction, and users don’t like having to click several times to do an action that could be completed in a single click. An example of this is a form having pre-filled fields based on previous visits to a page, or having the most common option already selected, so that they don’t have to make their selection and then click “Continue” or “Submit”. In cases where explicit choice is important, we make an exception—like when accidentally proceeding can have catastrophic effects like sending emails to the wrong mailing list. I believe this might have been the reason why we were fine with tab selection following what was focused (or perhaps it was the underlying behaviour of input
radio elements to begin with).
Having selection follow focus in tabs is not exactly catastrophic in our product, however, I wanted to exercise caution. So I made the decision to update the behaviour, and not have selection follow focus, thus allowing the user to make an explicit choice. An example of our tabs use is to categorise between different subscribers who are “active”, “unsubscribed”, “bounced”, or have marked emails as spam. Within each individual tab, we give the user the ability to search subscribers and perform bulk actions such as moving subscribers from “active” to “unsubscribed”. It is entirely possible that a user could have performed a search and used the keyboard to cycle through the tabs only to have the tabs change on them. This may require extra work to maintain the state of the previously selected tab mid-search, but above all, can create a frustrating and terrible experience for the user.
The behaviour of the tabs in Google’s Material UI allows you to use your keyboard to focus on each tab and then press Enter
or Space
to confirm your selection; there is also an option for selectionFollowsFocus
. I suppose having an option isn’t such a bad thing either, but the way we roll, we won’t be introducing that into our component as an option unless it becomes a widely used pattern. Meanwhile, I don’t see anyone complaining about manual tabs or explicit choice since we changed the behaviour. ✨
Related reading:
- Example of Tabs with Manual Activation (most useful in our learnings here)
- Example of Tabs with Automatic Activation
- ARIA: tab role
- Developing a keyboard interface
- Material UI Tabs: Accessibility
- Functional CSS Tabs Revisited (in depth on using the
input
element and styling it)
If I’ve made an error in this post, or you have something to add, let me know. As of writing this, the code blocks on my blog are visually unappealing and need some work—I am aware. 😅
Without a professional accessibility expert on our team, I think it’s good to periodically review what we’ve built and make sure we’re doing our best to adhere to accessibility guidelines. What have you learned in the realm of accessibility lately?
Leave a Comment