Pure CSS Tab Panel
Published on (01-22-2016).
Most Tab Panel widgets rely on a list of jump links followed by "sections". These links are styled as tabs, while the sections are styled as panels.
The problem with this approach is that semantically it is not the best fit, which forces screen-reader users to create a complex mental model. WAI-ARIA roles may help these users to make sense of that markup but still, I think it would be much better if POSH (Plain Old Semantic HTML) was used to convey the pairing of “tabs” and “panels”…
KIS (Keep It Simple)
A simple succession of headings and divs should lead users to assume - rightly - that those headings are followed by their respective section (by the content they "introduce"). This alone is enough for screen-reader users to make sense of the markup—after all, when it comes to content, this is the most common markup pattern on the Web.
From there, the challenge is to create a Tab Panel without breaking the experience of SR users along the way. We can use extra markup and all the CSS we want, but we do not want to create "meaningless" tab stops or actionable contols (i.e. "buttons" to show/hide panels).
As is often the case with CSS, the solution relies on a combination of specific markup and styles. Radio buttons are used as selectors to "show/hide" the panels and because they are pratically impossible to style, we use associated labels with these controls.
- The container that wraps all components of the widget is styled using
position:relative. This creates a containing block, meaning positioned children will use that ancestor as a reference.
- All headings but the first one are:
- styled with
position:absoluteand moved to the top of the container with
- styled with a
leftoffset (equal to the width of the previous tabs(s))—to align all "tabs" next to each other
- styled with
- Keeping the first heading in flow assures the proper positioning of the divs below (the panels)
- we use the pseudo-class
:checkedto target the panel we want to reveal. The selector looks like this:
input:checked + h2 + div(the
divrepresents the panels)
- The panels are made invisible but they are not hidden from SR users. For this we stay away from
display:noneand prefer to rely on the clip method. Another solution could be to use
height:1pxbut make sure to not use
0because Voice Over may ignore the panels (in doubt, always test).
With the extra markup required to make things work, it looks like this:
<label for="tab-1" tabindex="0"></label> <input id="tab-1" type="radio" name="tabs" checked="true" aria-hidden="true"> <h2>Tab 1</h2> <div>Panel 1</div>
What and why:
- The value of the
forattribute must match the control’s
id. This association assures that when a user clicks on the label its associated radio button gets checked.
labelis supposed to allow keyboard users to reach the next tab after tabbing through a panel (more on this below).
- The value of
namemust be the same for all radio buttons. This is to create a group that will allow options to be mutually exclusive (selecting a button in a group automatically unselects all other buttons in that same group).
- Panels appear according to which radio button is selected. We used
checked="true"on the very first button to reveal the first panel by default.
- Radio buttons create a tab-stop, so we use
aria-hidden="true"to make these invisible to SR users.
- There is no JS dependancies—this solution is perfect if you have no focusable elements in the panels (see cons below).
- The simple succession of headings and divs (vs. jump links and divs) is easier to comprehend.
- That markup structure is also more RWD-friendly as we can let everything stack together at narrow viewport width.
- Sighted keyboard users can navigate between tabs using arrow keys.
- Extra/non-semantic markup
- Tab widths are "magic numbers" (their
widthmust be hardcoded)
- Once sighted keyboard users leave the "tabs" they have no way to reach them again unless they tab back.
Note: the last point above would not have made the list if browsers were not broken when it comes to
tabindex. Because all browsers fail to create actionnable tab-stop, sighted keyboard users cannot easily navigate between panels.