Advanced ARIA tip #1: Tabs in web apps

The following article will describe how to properly create accessible tabs in web apps. This is important for both mobile and desktop web applications. Tabs are not native to HTML5, so if you simulate them, you’ll probably use other markup such as lists and list items to generate them. You will have to add WAI-ARIA markup to make these semantically correct. For non-touch-screen interfaces, you’ll also have to add keyboard support manually to make sure the experience is consistent with native apps.

This article assumes that you have at least a basic understanding of what WAI-ARIA is and how to apply attributes. This article will show you which attributes are appropriate for this particular task. If you do not yet know what WAI-ARIA is or want to refresh your memory, go and read, for example, this introduction.

To get tabs to work right, there are a few roles and attributes we’ll need:

This is the list of tabs itself. It indicates to screen readers that child elements are selectable tabs. It is a container role and allows screen readers to count the number of actual tabs inside.
An actual tab. This must be a keyboard-focusable item. It must be focusable directly, not one of its children.
a boolean attribute that indicates whether the current tab (in this case) is the selected one. aria-selected is applicable to other types of items such as option items as well.
Indicates which element is being controlled by this particular item. We’ll use this to connect a single tab to its actual tab panel.
A single tab panel. This is similar to a dialog page, it contains various controls.
The attribute to indicate where the tabpanel gets its label, its title, so to speak, from.
aria-describedby (optional)
The element(s) to provide the descriptive text, for example explanatory dialog text, for this tabpanel.
A role used to remove certain intermediate objects from the screen reader’s view, but which make semantically sense to keep in the HTML.

The code without WAI-ARIA

<ul id="tabs">
<li><a id="tab1" href="#" onclick="showTab(1);">Tab 1< /a></li>
<li><a id="tab2" href="#" onclick="showTab(2);">Tab 2< /a></li>
<li><a id="tab3" href="#" onclick="showTab(3);">Tab 3</a></li>

Obviously, you’d add logic to that showTab() function to show and hide the tabs and keep track of which one is currently selected, adjust their styling etc.

Adding proper semantics

As it stands, this would render the tabs as a bunch of links in an unordered list, and the tab panels as mere block containers with controls in them. To now add proper semantics to that, so that screen readers recognize these as tabs, we’ll have to change the same code snippet as follows:

<ul id="tabs" role="tablist">
<li role="presentation"><a id="tab1" href="#" onclick="showTab(1);" role="tab" aria-controls="panel1" aria-selected="true">Tab 1< /a></li>
<li role="presentation"><a id="tab2" href="#" onclick="showTab(2);" role="tab" aria-controls="panel2" aria-selected="false">Tab 2< /a></li>
<li role="presentation"><a id="tab3" href="#" onclick="showTab(3);" role="tab" aria-controls="panel3" aria-selected="false">Tab 3</a></li>

The above code snippet does the following:

  1. It adds the role of tablist to the ul element, indicating that the children are tabs.
  2. Adds the role presentation to each of the li elements, indicating that the screen reader should ignore the list items themselves.
  3. Adds role of tab to each link, re-mapping their roles to the intended screen-reader recognizable element type.
  4. Adds aria-selected to each of the tabs. When you switch tabs in your JS code, update these to reflect the new state of each. Only one can be selected at any given time, so the values of two should be false, and only one should be true.
  5. Adds aria-controls to each, indicating which panel is referenced by the tab.
  6. Adds a role of tabpanel to each of the div containers.
  7. Adds aria-labelledby referencing the actual tab’s name given to the a elements by the inner text above as labels for the panels.

What your JavaScript now needs to do is:

  1. Hide the old tab, by styling the panel1, panel2, or panel3 container as display:none;. Do not just move the panels out of the visible view port, as this will not hide them from screen readers! Set the tab1, tab2, or tab3’s aria-selected attribute to false.
  2. Make the new panel1, panel2, or panel3 visible. Set the tab1, tab2, or tab3’s aria-selected attribute’s value to true.

The best keyboard interaction model is this:

  1. Left and Right arrow keys should move focus to the new tab, but not yet select it.
  2. Space should actually perform the hiding and un-hiding of the tab panels and adjust the aria-selected attributes. This is how Mac OS X applications with multiple tabs usually do it, for example many multi-tab panels in the System Preferences. This makes sure the user can change focus multiple times without each focus change triggering a dynamic update and possibly network traffic. Only an explicit step to select a tab should then actually trigger the change, and traffic. Mouse or touch can trigger both at the same time.
  3. Tab should immediately move to the first control within the tab panel. It should skip over the remaining tabs.

Common questions

Why links as tabs?
Because they give you focusability for free, without you having to fiddle around with tabindex values.
Why list items?
Because this is still a list, and only list items are valid children of an ordered or unordered list. 😉 And because this gives you more flexibility in styling.
Can I use images instead of text?
Yes, provided the images have alt attributes with proper labeling text set. Do refrain from using the title attribute.
Why hide the unselected panels via display:none;?
Because otherwise, they’d be cluttering up the screen reader user’s view even though they weren’t visible. Screen readers would be able to set focus to items they aren’t supposed to at the moment and could totally mess up your app logic. Moreover, many screen reader actions could produce unpredictable results because simulated clicks could end up at random screen coordinates. In addition, truly hidden panels free up memory, which is especially handsome on low-spec mobile devices.

You can use other structural elements if you wish, provided you set the ARIA roles and attributes as described above, and also remove those elements from the screen reader’s view that are not needed.

When to not use tabs semantics

There are many circumstances where tabs are not the appropriate semantics. For example, if you have a web site, not a web application, that has categories such as “Home”, “Products”, “Support” etc., which may look like tabs, but actually load new pages, then these are not tabs in the intended sense, but should in all cases remain links, because that’s what they are. Bryan Garaventa wrote more about this here.

If it were marked up correctly, the mobile Twitter site would be an ideal candidate for appropriate tabs semantics. Specifically, the “Home”, “Connect”, “Discover”, and “Me” items at the top. They don’t open new pages, but switch a view dynamically instead.


27 thoughts on “Advanced ARIA tip #1: Tabs in web apps

  1. I spent yesterday afternoon trying to make sense of ARIA tabs (and posted a sketch of my understanding), so your timing is perfect (or 24 hours too late). Thanks for that.

    I have a couple questions.

    1. If I use either aria-labelledby (on the tabpanel) or aria-controls (on the tab), do I need to use the other? Essentially, do I need to use both? If so, then I totally mis-read the spec.

    2. I have different HTML. I use [h2]s for the tabs and the content sits directly beneath them. Since my tabs are folded in with my content, can I have tabpanels embedded within a tablist (that are peers to tabs)?

    Example pseudo-code (only including roles), repeated for each tab:

    [article role=”tablist”]
    [h2][a href=”tab1″ role=”tab”]Foo[/a][/h2]
    [div id=”tab1″ role=”tabpanel”]uh oh[/div]

    Happy to link to the page where this is in play, I just don’t want to spam your comments with links to my site.

  2. Hi Adrian,

    1. Yes, you have to use both. aria-labelledby provides a name for the element/control being labeled. aria-controls provides a means for screen readers to jump directly from one element to the controlled element, as is, for example, implemented in JAWS. The former is used by virtually all screen readers today, the latter by some, but not all. But it is good practice to provide this linkage.

    2. I’d check the spec, but I am under the impression that tab panels should not be siblings to tabs within a tab list. But I am not absolutely clear on that myself, just have never seen this in the wild. What you are doing there sounds more like an accordeon style thing to me where more than one thing could, in theory, be expanded. Tabs, on the other hand, are mutually exclusive. Only one can/should be visible at any given time.

  3. Huge thanks for the feedback.

    1. I’ll update my whiteboard on Monday.

    2. Thanks to the magic of RWD, my example is an accordion on smaller displays and tabs on larger displays. My plan is to treat it as tabbed in the context of ARIA mapping regardless of screen size. If you are curious, I built it for (no ARIA bits in there yet).

  4. Maybe a bit off-topic, but: speaking of mobile applications, is there any kind of assistive tech support baked into Firefox OS at this point?

  5. @Patrick: Not “at this point”, meaning in version 1.0. There were certain challenges involved unique to the fact that this is an operating system, not “merely” a browser. For example, proper communication to speech synthesis from JS, which is being implemented right now. We are working hard to come out with a beta people can try soon’ish.

    In the meantime, all of the UI pieces of a web app can be tried on Firefox for Android, which since version 17, supports all currently supported Android versions down from Honeycomb up to Jelly Bean. The pieces that drive this part of Firefox OS are identical in Firefox for Android, so if your web app UI gives you expected results in TalkBack on Android, you can be sure it will also work on Firefox OS when it becomes accessible.

    In fact, many concepts can also be tried on Windows with NVDA, since the accessibility core is the same across all the supported platforms.

  6. Great article, thanks.

    The only thing I’m not really comfortable with is using list items rather than buttons to create your tabs. I agree they should be contained in a list to group them, but by only using list items to do this, you’re actually telling the screen reader user that activating these triggers will lead the user somewhere while in fact, it’s only creating a change of context.

    I agree with you are more easily styled, but I believe it would be more semantically sound if the action was called on a inside those list items, rather than on the directly.

  7. Editing because WP ate my html tags…

    Great article, thanks.

    The only thing I’m not really comfortable with is using list items rather than buttons to create your tabs. I agree they should be contained in a list to group them, but by only using list items to do this, you’re actually telling the screen reader user that activating these triggers will lead the user somewhere while in fact, it’s only creating a change of context.

    I agree with you LI are more easily styled, but I believe it would be more semantically sound if the action was called on a BUTTON inside those LI, rather than on the LI directly.

  8. Hi Marco,

    Thanks for this clear explanation of how to implement an accessible tab-view.

    What I’m doing now is start with headings + divs and then use JS to rearrange things so the headings (tabs) can work as tabs. The headings follow each other and then one panel is displayed at a time. Also, everything is marked up with WAI-ARIA attributes as you described in this article.
    I think the advantage of this approach is that without script support, the document is a succession of headings with their divisions which I think makes more sense than a list of in-page links.

  9. Hi Dennis,

    oh they aren’t carried out on the li. The actions ought to be carried out on the a. They could be buttons instead as well, but because of role=”tab”, the actual HTML is masked by the role of “tab”. The li itself is never exposed to screen readers at all due to the role of “presentation”, and since the li itself isn’t focusable, that means the list item accessible actually never shows up in what the screen reader sees.

  10. I’ve been doing the same as @Thierry (headings and divs, rearranged using JS) as I like the fact that everything behaves nicely without JS enabled. However, saying that, I would like to ask an ARIA-related question: would it be better to use anchor links for the tab link elements (<a> id="#divId" ... >) rather than simply use “#”? Or is it the case that the aria-controls attribute is enough? To me, it ‘feels’ better to link to the tab content, but I would like to know if I am wrong 🙂

    Really nice article, by the way

  11. Oh right, great points indeed. Just realizing how much aria actually conditions the HTML used and how much it changes everything from the screen readers’ perspective. So what you’re saying between the lines is that by relying on aria so much, minding about semantic markup for screen readers becomes a little less important…

  12. What I don’t like about the way screen reader NVDA and Jaws handle the “tab” role is that they force the screen reader into application mode. This means the user must then escape from application mode before carrying on with their interaction, and it makes things a bit more confusing for the novice screen reader user. Check out the demo at:
    and an accordion demo at:

    I use buttons as tabs, and show state with aria-pressed.

  13. Hi Rich,

    the question is if the use case the tabs are being used for is actually warranting the tabs, which are clearly a desktop/application kind of concept, or if it is just a matter of showing/hiding certain kinds of content. An accordeon, the way I understand it, is no use case for role=”tab”. For role=”tab”, application/focus mode is definitely appropriate, since a certain kind of user interaction via the keyboard is expected from desktop OS experience. So the question is always if the right roles are being used for each use case.

  14. Good article.

    Why not use the ID values of the tabpanels as the values for the href attr’s used in the anchors in the tablist e.g. href=”panel1″ so you’re linking them up if JS is disabled, not really helpful seeing how close the panels are to the links but at least they’re being used semantically. Or change the anchors to buttons if the anchors arent pointing anywhere as then its just an actionable item i.e. a button.

  15. Great article, Marco. Two issues:
    1. Agree with Chris above; for non-JS support, add IDs of panels to anchors.
    2. Your keyboard interaction makes sense, but it’s different than the WAI guideline as well as the OpenAjax example. Unless pulling a lot of Ajax content, I suggest sticking with their pattern i.e. the panel content displays when tab focuses, not after Space or Enter key.

  16. @WebAxe, thanks for your comments!

    Regarding Non-JS: Does anyone really still care about this stuff? I am absolutely convinced that non-JS solutions are no solutions at all, but rather very basic, down-scaled crutches that don’t deliver a rich and screen-reader-friendly experience at all.

    Regarding the “new tab panel on focus” issue: As long as it is made sure that focus doesn’t change automatically to the tab panel, but that the user has to consciously tab forward into it, that would be OK with me, too. Personally, ever since I started using Macs, I think the Windows and Linux interaction model rather backwards. I much prefer the Mac way of doing this. I know it’s against some of the authoring stuff, but then, this is a personal blog and expresses my opinions on matters. 😉

  17. This is gonna sound nitpicky, but “Boolean attribute” has a very specific meaning in HTML, and @aria-selected does not fit the description. The @aria-selected attribute must have a token value of “true” to be considered selected, whereas a true Boolean attribute, like @selected in HTML, would be considered selected if it was present at all, even with a contrary value like selected=”false”… Boolean content attributes also have “reflected DOM attributes” that stay in sync with the state of the content attribute, so for example, element.selected would return true.

  18. Great article. To the point and useful. To bad there’s no demo-page to try it out.

    Tabbed navigation has a been a thorn in my eye for quite some time now.
    I’ve been trying to find an implementation that’s usable AND understandable for a screenreader user.

    The French (Atalan / AcceDe Web) came up with this, which I like a lot:

    If differs from you approach because it’s not possible to navigate between tabs with the arrow keys. Only with the tab-key.
    I would rather make both possible.

    In your description of the best keyboard interaction model, you state that tab should immediately move to the first control within the tab panel and skip over the remaining tabs.

    But how can a keyboard user (not a screenreader user) know that he has to use the arrows to navigate? Arrow keys are not commonly used to interact with the content of a website. Tab-key and enter-key are. Opera is an exception who replaced the tab-key with Shift + arrows for keyboard navigation. But that’s another story.

    Shouldn’t all controls of tabbed navigation work with tab- and enter key, keeping things simple and straightforward but most importantly predictable?
    I would suggest making both arrow keys and tab-key available to switch between the tabs. I don’t see a problem for this combination.

    There are some big problems not mentioned in you article making this a very hard nut to crack:

    1. Only the latest version of screenreaders support ARIA, announce tabbed navigation correctly and only when it’s implemented correctly.

    2. I believe the majority of users have an outdated screenreader because of the price to update (not the case with NVDA & VoiceOver offcourse) in combination with the fear of change.

    3. I believe the majority of screenreader users don’t know what tabbed navigation is or does. Only power users and web professionals who use a screenreader (for testing) do.

    4. Automatically switching a screenreader in application mode adds extra confusion.

    5. And last, there’s no consensus in how to implement tabbed navigation, so it’s different on every website. Adding ARIA to the default jQuery tabs would be a great start.

  19. But how can a keyboard user (not a screenreader user) know that he has to use the arrows to navigate? Arrow keys are not commonly used to interact with the content of a website. Tab-key and enter-key are.

    Tabs are not native to web sites by default. In fact, there is no HTML5 widget that resembles these. So the only keyboard interaction model known is the one known from regular dialogs, for example in the Windows Control Panel or Microsoft Word’s Tools/Options dialog, or Firefox’s Tools/Options dialog. The user is supposed to think in such web applications, that they’re interacting with a user interface resembling that of a desktop application. And as such, keyboard interaction models known to that environment should be emulated.

  20. A couple of notes regarding the comments from others.

    A recent (2012) survey from WebAIM of screen reader users shows that 98% have JavaScript enabled. Visit the WebAIM Screen Reader Survey 4 for more info.

    Also, ARIA support in screen readers is gaining significant traction (JAWS officially supports all of the ARIA used in the above implementation; however, you will need to use INSERT+ALT+M to move to the control element if using JAWS). One of the issues is which browser is being used; regardless, ARIA is here… embrace it. It can only help.

  21. About non JS use case, I didn’t mean for screen readers. It’s just good practice and benefits the few who may benefit for a variety of reasons. We’re supposed to be inclusive, right? I don’t think adding #ID is much work to do so…

  22. Great article, Marco! Extremely helpful, I’ll be sharing this with the other developers I work with.

Comments are closed.