I'm probably the last person to realize this; but React is really great! The last few weeks I've been playing with it, and so far it's been a really enjoyable experience.
I've been wanting to use it for something, and since another of my favorite topics is accessibility, I thought I should try to create a few different accessible components. It will mainly serve as a way for me to increase my React knowledge, but will hopefully be useful to others in the end.
The first component I decided to create is a tab list. I will include working examples from CodePen so that you can try out the difference between an accessible tab component and an inaccessible component. If you are using a Mac, I recommend you try it with VoiceOver as well.
The first step is to create the basic structure for the tabs. In React I want something like this:
I want that to generate HTML like this:
The first example (see below) is probably where a lot of people would stop. You can click each tab and it will display the corresponding tab panel. There is some default keyboard support; the user can move between the tabs using the tab-key and hit Enter to select a tab. But a blind user would never know that it is a list of tabs he/she is interacting with.
See the Pen Accessible tab component in React - step 0 by Andreas McDermott (@andreasmcdermott) on CodePen.
Adding the correct roles
If you try the next example you'll notice that the screen reader now correctly reads "tab 1 of 3" when on the first tab. This is achieved by assigned the elements their correct roles.
There are a lot of roles. Some roles have dedicated elements (like
<button> automatically has
role="button"). Other roles have no corresponding elements (like tabs). The roles we'll use are "tab", "tablist" and "tabpanel".
role="tab" has to be the direct children of an element with
role="tablist". As you could see in my example above, this isn't the case. The
<ul> will be the tablist, and the link will be the tab. To get around that I can assign
role="presentation" to the list item. That tells the screen-reader that the element can be ignored. Without the presentation role, each list item would be considered its own group of tabs. So each tab would be "tab 1 of 1".
After applying the correct roles, our HTML structure is:
The result is immediately a lot better and more accessible than the initial version. But we are still missing some necessary aria attributes and proper keyboard navigation.
See the Pen Accessible tab component in React - step 1 by Andreas McDermott (@andreasmcdermott) on CodePen.
There are a lot of different aria attributes. Some can be used on any element, others are intended for elements with specific roles. We will use
aria-labelledby which are general attributes (can be used anywhere). We will also use
aria-controls which are used for interactable elements.
Aria-hidden="true" is the equivalent of
display: none for the screen-reader. We are already hiding the inactive tabpanels using
display: none and screen-readers should consider these elements hidden as well, but I've had issues with certain screen-readers ignoring this in the past. To avoid that I'm adding
aria-hidden="true" as well to the inactive tabpanels.
When the screen-reader highlights the tabpanel, we want it to include the tab's title. That is what
aria-labelledby is for. The value of the attribute should be the id of the tab.
Aria-label can be used for the same purpose. The difference is that it takes the label text instead (but because we don't want to duplicate the tab title,
aria-labelledby is a better choice here).
Aria-selected="true" is added to tell the screen-reader which tab is active. It is important that the inactive tabs have
Aria-controls is not strictly needed here, but I'm adding it anyway. Both
aria-labelledby are used to associate the tabpanel with its tab. I've already added
aria-labelledby, but it doesn't hurt to add
aria-controls as well.
The rendered HTML will now be:
And the example:
See the Pen Accessible tab component in React - step 2 by Andreas McDermott (@andreasmcdermott) on CodePen.
This is the last step to make the tabs fully accessible. The WCAG has specified how keyboard navigation should work for different components. For tabs there are mainly two things to consider:
- When the user uses the tab-key to enter a tablist, focus should be placed on the active tab. Hitting the tab-key again should leave the tablist.
- Left and right arrow keys should navigate between the tabs in a tablist.
WCAG also specified that when using the arrow keys to move between the tabs, the tab should also be automatically activated. I've seen other sites that recommend that moving left and right only highlights the tab, and enter- or space-key is required to select the tab. I've chosen to follow WCAG's recommendation here.
For the first item, we will add
tabindex="0" which tells the browser that the element should be added to the tab order. It is also possible to set a value greater than 0, but it is not recommended since that will alter the natural tab order.
This is the final HTML structure:
You can try the final example below:
See the Pen Accessible tab component in React by Andreas McDermott (@andreasmcdermott) on CodePen.
That's it. We now have fully accessible tabs. I hope you learned something! Feel free to use this on your site.
And if you have any suggestions on how my component could be improved or any other comments, just tweet at me!
I would also appreciate suggestions on what component I should implement next!