By now tabs component is a very old UI invention and has been around quite a while. We've seen many examples of how tabs should not be done (multirow tabs anyone?), while lately accessibility message has finally gotten through as we now see ARIA mentioned in almost every UI component library out there. This is great development as a11y is something I've tried to get right years ago, but gotten it wrong as the information around the web has been awfully conflicting and open for incorrect interpretation. While there are still sources that are awful the increase of good information allows anyone checking multiple sources to correct their mistakes.
During the jQuery days, right before React became a thing, the holy grail of tabs design was the following:
- Structure tabs as single components instead of splitting into tab bar container and panels container.
Since around IE9 level of browser capabilities all of this was possible to achieve! There were some limitations of course, starting from the fact the layout had to be based on hacky CSS, but that was all we had before flexbox and grid anyway.
<div class="tabs"> <div class="tab"> <input class="tab-radio" type="radio" id="tab-X" name="tab-group-Y" checked> <label class="tab-label" for="tab-X">TAB TITLE</label> <div class="tab-panel"> <div class="tab-content"> TAB CONTENT GOES HERE </div> </div> </div> </div>
This structure was very hard to make work with CSS: it was much easier to just have tab labels inside their own container and the related contents in their own. The advantage of the above structure is that it keeps related content in the same context. This makes it much easier to add or remove tabs as all related code is in one place.
With (now legacy) tricks the above HTML can be made appear as tabs:
font-size: 0to remove space between
inline-blockelements (tab labels).
inline-blockelements to align on the same row.
- Radio elements must be hidden, but so that keyboard access is retained.
inline-blockso they get to their own row.
overflow: hiddenand other hacks to workaround cross-browser issues (the price of IE6, IE7 and IE8 support!)
float: left, which together force the content to jump below the labels.
I must admit I still love CSS hacks and working around limitations! :) Modern CSS, blergh, you can do everything without a headache __;; (nope, not serious).
The thing that I got badly wrong in the code above is that I used
div elements far too much: I should've used
li for each tab as this tells the number of elements in screen readers. Each example that lacks semantically correct elements is a bad example, so that is certainly something I regret: one should do HTML properly even when the main focus is showing a tricky CSS sample. This is better for everything: search engines, screen readers, and understandability for a developer who reads the code later – it is super awful to read HTML where everything is a
div, you have no mental anchors anywhere!
In the other hand Chris Coyier's original code sample claimed accessibility by hiding the radio elements entirely by using
display: none. This indeed made the tabs appear as just one continuous content to a screen reader so they wouldn't know about tabs at all and got access to all content, but you also lost native keyboard access for switching between the tabs. The reason for having tabs is also lost in this case: you use tabs to group information or functionality that you let user have optional access to. This point isn't fulfilled if everything is just a long block of content.
To fix these problems we can use ARIA attributes! So lets upgrade that old HTML:
<ol aria-label="Choose content with arrow keys:" class="tabs" role="tablist"> <li class="tab"> <input aria-controls="tab-1-panel" aria-labelledby="tab-1-label" aria-selected="true" checked class="sr-only visually-hidden" id="tab-1" name="tab-group" role="tab" type="radio" /> <label class="tab-label" id="tab-1-label" for="tab-1">SELECTED</label> <div class="tab-panel" id="tab-1-panel" role="tabpanel" tabindex="0" > VISIBLE CONTENT </div> </li> <li class="tab"> <input aria-controls="tab-2-panel" aria-labelledby="tab-2-label" aria-selected="false" class="sr-only visually-hidden" id="tab-2" name="tab-group" role="tab" type="radio" /> <label class="tab-label" id="tab-2-label" for="tab-2">UNSELECTED</label> <div aria-hidden="true" class="tab-panel" id="tab-2-panel" role="tabpanel" tabindex="-1" > HIDDEN CONTENT </div> </li> </ol>
Okay, that is a lot of new stuff! I'll go through things extensively.
ol: you need to tell the context of the tabs somewhere.
visually-hiddenseem to be the modern conventions for visually hidden content that is targeted for screen readers. You use the one you like, or your own.
aria-controls: tells which panel is controlled by a tab.
aria-selected: indicates the panel is selected (checked is just HTML state).
aria-labelledby: input element can have multiple labels, so let screen reader know what this is (could also use
aria-labelto give different kind of instruction for screen reader user).
tabpanelare the three required ones.
tabindex="-1"in panel to hide content that is not active.
tabindex="0"on active panel content: this makes the content focusable and tabbable. The reason I would like to do this as a developer is to be able to remove active focus indication from a clicked tab (thus still allowing clear focus indication to appear in keyboard usage), but I'm still unsure whether this is the right thing to do.
- Not having
tabindex="-1"in unselected tabs: radio element appears kind of as one element, so you can only access individual items via arrow keys.
- Using radio elements as tabs: this structure is built to preserve as much native browser behavior as possible (even when using JS). It could be argued that
labelelements should be the ones with
role="tab"and all the related aria attributes, and then hide the radio elements from screen readers entirely.
- You could indicate
lielements, but is that the correct element, and is doing that useful at all? It could be useful for styling though!
- You could give
role="tablist"element to indicate horizontal and vertical tabs, but that is yet another thing I don't know if it has any practical value. Yet another thing that could be used for styles via CSS!
There seems to be support for
aria-disabled. I can somewhat understand it, but I've started to notice that most often it might be better to not display unavailable option at all. Avoiding disabled makes for both a greatly simpler design and less confusing experience, but I have to admit this is a thing I still need to do further reading on.
But you can make things work. In React for example you can simply toggle different rules after component has mounted, so when rendering server side HTML you would end up with this result instead:
<ol class="tabs" role="tablist"> <li class="tab"> <input aria-controls="tab-1-panel" checked class="hidden" id="tab-1" name="tab-group" role="tab" type="radio" /> <label class="tab-label" id="tab-1-label" for="tab-1">SELECTED</label> <div aria-labelledby="tab-1-label" class="tab-panel" id="tab-1-panel" role="tabpanel" > VISIBLE CONTENT </div> </li> <li class="tab"> <input aria-controls="tab-2-panel" class="hidden" id="tab-2" name="tab-group" role="tab" type="radio" /> <label class="tab-label" id="tab-2-label" for="tab-2">UNSELECTED</label> <div aria-labelledby="tab-2-label" class="tab-panel" id="tab-2-panel" role="tabpanel" > VISUALLY HIDDEN CONTENT </div> </li> </ol>
Here is a summary of changes:
olas it instructs JS-enabled behavior.
aria-selectedremoved from radio element.
classis changed to
display: none) to disable screen reader access to tabs.
aria-labelledbyis now in the
role="tabpanel"element so screen reader will tell the context of content.
tabindexare fully removed from
Essentially all content is then available, although as one long span of content, and there is no indication to a screen reader that these are actually tabs.
Do you know better about all of the above than I do? Let me know in the comments!
react-tabbordion v2 is done.
A thing I've been researching for v2 is all the different HTML structures out there. Because so far most of the Tabs and Accordion components out there do force you into specific structure, which I think leaves another niche I would like to fill: let user of a component focus on building tabs the way they want, and for the need they have.
The reason for my thinking is that not one Tabs component answers to all the needs. Looking around the web I can find several kinds of solutions:
<ol role="tablist" />+
<li role="tab" />: this has minimal HTML footprint while being a proper list.
<button role="tab" />: probably the most common one, and often with no list elements.
<nav role="tablist" />+
<a href="#" role="tab" />: allows for tabs that are links to another HTML page (optionally, when JS is disabled). Haven't seen any that would be presented also as list elements.
<li role="tab" /> option allows for only one usage: all content must be pre-rendered in HTML, and the
tablist must be entirely hidden from screen readers, only allowing access to the content as one span of content. However, as there is no state in HTML, there should be no
tabs rendered: only all the content within the panels in one visible list. The only reason to use this would be the compactness of the HTML, thus shorter and faster load times.
tab items focusable.
<a href="#" role=tab" /> option provides another kind of possibilities. You could have multiple forms within a single panel, you can have the tab as a true link that would serve another HTML page for a panel, and you can have the links as anchors to panels that are rendered into the HTML. You could also mix and match, and you can certainly keep the links clickable even when JS is disabled as you can make everything work visually even with only CSS (using
:target to show the correct panel and indicate active tab).
As the final option we could compare these to the radio list structure. The advantage of radio list is the most solid CSS that it can provide via
<li role="tab" />least syntax, but depends heavily on JS implementation, all panels must be rendered to HTML, content would flash upon JS hydration as you must have all content visible with no-JS (unless you try to workaround using
<button role="tab" />would work as form, but cannot have forms inside panels. Each panel should be separated to their own URL.
<a href="#" role=tab" />gives most possibilities: you can indicate active state via CSS, you can have panels that are only loaded on demand, and you can have panels that are pre-rendered into HTML. The CSS functionality without JS wouldn't be optimal, though.
<input type="radio" role="tab" />(or
<label role="tab" />) has the best CSS-only state possibilities, but all panels must be rendered to HTML in advance.
Did I get something wrong? Did I miss a HTML structure that is out there in the wild? Let me know!
But that kind of mentality is the reason for so many issues we have with the web, and with humanity in general: when you ignore something, you're eventually ignoring people. In business sense that means lost visitors, and in turn lost customers. In practical sense you're most likely too busy to care, if not that, then the other options are to be lazy, or actually being a person who doesn't care. Personally I've certainly been in the too busy department for far too long!
These days we have reached a point where standards are very good, and we have far less browser issues to worry about. Internet Explorer 11 is still a thing for some of us, but even it has enough support that you can make tolerable fallback styles and functionality for it.
All this leaves more room to focus on stuff that remains hard due to required amount of knowledge:
- Solid CSS architecture
- Semantic HTML (or meaningful in case you think semantic has lost it's meaning; pun intended)
Most of these fronts are about basic usability: keeping things working under all conditions, and making stuff available for everyone in every possible way. You provide much better quality and experience to end users by taking these things to account. Although the CSS part is more of an issue for large scale development.
And here I've been talking about tabs, ending up talking about how to make the world a better place :)