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”…
TL;DR: Pure CSS Tab Panel on codepen
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.
Progressive Enhancement
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).
Going JS-less
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 CSS
The logic:
- 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:absolute
and moved to the top of the container withtop:0
- styled with a
left
offset (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
:checked
to target the panel we want to reveal. The selector looks like this:input:checked + h2 + div
(thediv
represents the panels) - The panels are made invisible but they are not hidden from SR users. For this we stay away from
display:none
and prefer to rely on the clip method. Another solution could be to useoverflow:hidden
withheight:1px
but make sure to not use0
because Voice Over may ignore the panels (in doubt, always test).
The markup
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
for
attribute must match the control’sid
. This association assures that when a user clicks on the label its associated radio button gets checked. tabindex="0"
on eachlabel
is supposed to allow keyboard users to reach the next tab after tabbing through a panel (more on this below).- The value of
name
must 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.
Pros
- 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.
Cons
- Extra/non-semantic markup
- Tab widths are "magic numbers" (their
width
must 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 label
and tabindex
. Because all browsers fail to create actionnable tab-stop, sighted keyboard users cannot easily navigate between panels.
See the Pen Pure CSS Tab panel by Thierry (@thierry) on CodePen.