Tab Panel, the right way...

Published on (09-23-2016).

Tab Panels have been my pet peeve for years [1], mostly because of the arguments people come up with to rationalize the use of their companion jump links.

Let me explain…

The Big Lie

In preparation of a new article...

Why do we use "jump links" for Tab Panels?

— Thierry (@thierrykoblentz) September 2, 2016

and the answer is…

We use jump links for styling!

Yes, jump links are nothing more than a hack. We did not adopt them for a noble cause like Accessibility or Semantics, we embraced them because they facilitate the display of tabs atop panels.

This is the reason why “all” Tab Panels use jump links today; why we have a ARIA role for it; and why we’re told that’s “How [it] Should Work”; …

We, as a community, decided that the ends justified the means and it was okay to go with this convoluted solution. We sacrificed Semantics and Separation of Concern simply because we needed to keep the tabs in flow while displaying them side-by-side.

Actually, we’ve sacrificed more than that as these jump links do not help with keyboard navigation. This is because most people expect to tab (no pun intended) through Tab Panels following this simple sequence:

  1. Tab 1
  2. Panel 1
  3. Tab 2
  4. Panel 2
  5. Tab 3
  6. Panel 3

But since the jump links come first, source order is more like:

  1. Tab 1
  2. Tab 2
  3. Tab 3
  4. Panel 1
  5. Panel 2
  6. Panel 3

This the reason why we have to manage focus in Tab Panels via tabindex="-1", to make sure tabbing from a tab sends the user to its associated panel instead of the next tab. Note that this keyboard behavior is even more confusing when that markup is styled as an accordion.

Heydon Pickering explains the issue in a comment on How Tabs Should Work:

The TAB key is preserved for moving between the active TAB and the open panel. This means inactive tabs should have tabindex=”-1” and be focused/activated only programmatically (via the aforementioned arrow keys).

The reason for this is that keyboard and AT users should be afforded a direct route to the chosen content without having to cycle through all the tabs in the tablist each time.

This is a poor fix though because even if it helps users reach the panel it does not prevent them from leaving the widget as they leave that panel. And the experience is worse if there is no focusable element inside the selected panel, because in this case users go directly from the tab to the next focusable element on the page—wherever that is!


TL;DR: see demo below.

See the Pen oJEAF by Pankaj Parashar (@pankajparashar) on CodePen.

Tab Panel And Accordion—Même Combat!

In my opinion, the fact that we do not use jump links with Accordions is the strongest argument against their use in Tab Panels. If we do not use jump links with Accordion it is simply because we do not need to.
The markup for Accordions is a simple succession of heading/div pairs (or even dt/dd) which facilates both styling and keyboard navigation:

      <h3>Header 1</h3>
      <div>Content related to header 1</div>
      <h3>Header 2</h3>
      <div>Content related to header 2</div>
      <h3>Header 3</h3>
      <div>Content related to header 3</div>

That’s POSH (Plain Old Semantic HTML) in all its beauty. Everybody wins! All users can build a simple mental model and authors can implement a basic loop for creating heading/div pairs.

Now, because we can’t easily style the above as a Tab Panel, we got creative:

    <h2>Tab Panel</h2>
      <li><a href="#panel-1">Header 1</a></li>
      <li><a href="#panel-2">Header 2</a></li>
      <li><a href="#panel-3">Header 3</a></li>
    <div id="panel-1">
      <h3>Header 1</h3>
      <div>Content related to header 1</div>
    <div id="panel-2">
      <h3>Header 2</h3>
      <div>Content related to header 2</div>
    <div id="panel-3">
      <h3>Header 3</h3>
      <div>Content related to header 3</div>

That’s a big step up in terms of mental model. And note that these jump links may not even make sense at all depending on the content of the widget and its positioning on the page (i.e. jump links that can’t make the page scroll).

As a side note, check the Simply Accessible example to see how jump links create redundancy between the headings in the panels and their related tab.

Visual Clutter Started It All

Designers came up with widgets like Accordions and Tab Panels to reduce a component visual “footprint”. In other words, the difference between these widgets is related to display, not to semantics; so from a markup perspective, the HTML should be identical for both:

    <h2>Tab Panel or Accordion, it doesn't matter</h2>
    <div class="widget tab-panel | accordion">
      <h3 class="tab | header">Header 1</h3>
      <div class="panel">Content related to header 1</div>
      <h3 class="tab | header">Header 2</h3>
      <div class="panel">Content related to header 2</div>
      <h3 class="tab | header">Header 3</h3>
      <div class="panel">Content related to header 3</div>

That’s it! A common structure allows us to easily style the component as a Tab Panel or an Accordion, think CSS Zen Garden! #win

Trying Our Best

The best approach may be to start with jump-link-less markup to which we’d add ARIA attributes like in this demo from Leonie et al with the exception of the keyboard mechanism which—to me—seems to be more geared toward advanced users. Instead, we could rely on the arrow keys to cycle through the tab items and at the same time support tabbing through those items and allow users to select a tab to move focus to its associated panel—the same way things work on Paul Adam’s excellent demo.

The Details

The recipe for the best user experience:

  1. Start with POSH, a simple set of heading/div pairs
  2. Add ARIA (role, aria-selected, aria-hidden, aria-labelledby) and tabindex attributes using JavaScript as these would be confusing if there was no behavior attached to the widgets. Use JavaScript to also plug a class on the component according to its type (accordion or tabpanel).
  3. Style the widget using the selector inserted by the script, this way containers are not hidden if there is no script support.
  4. Use cursor to style the tabs because those behave like buttons (default), not links (pointer).
  5. In case the component is displayed as a Tab Panel, style it as an Accordion before its tabs start wrapping (to accomodate user’s font-size) or at narrow widths.
  6. Do not mess with the tab key, let users tab through the tabs/headers and visible panel(s), all according to source order.
  7. Allow users to switch between tabs/headers using arrow keys (left/right and top/bottom). home/end keys should focus on the first/last tab.
  8. Reveal panels as a user cycles through the tabs
  9. For users who navigate between the tabs via the tab key—reveal the associated panel through click and keypress events (enter and space bar).
  10. Move focus onto the relevant panel whenever keyboard users select a tab via the enter or space bar keys.
  11. Provide a means for keyboard users to escape the widget (as we do with “modals”). This way users have the option to leave the component with a specific panel opened or to go back to the first tab if they wish.

Yes, I know about #11, discoverability is an issue but I think this approach is better than not having anything at all. Besides, the esc key could become a common implementation across widgets as a means to “step out” of them. Maybe it would be best to make the widget focusable and use aria-label to convey some extra info?


Random thoughts

I really dislike the fact that we start with a simple succession of heading/div pairs—which is simplification of the information at its best—but then manage to create a much more complicated model for SR users. It’s as if progressive enhancement went wrong…

The Irony

We use these widgets and hide their panels to preserve screen estate—but screen-reader users could not care less about that.

We use tabindex="0" to allow keyboard users to navigate through the tabs—but screen-reader users already have a means to navigate through headings.

At this point, I’d argue that SR users would be better off if the script did not attach tabindex, ARIA attributes, and events to the markup.

The Conundrum

Note that the problem is not related to the fact that we hide the panels—as we could do that for sighted users only. No, the main issues are related to tabindex, which signals that these elements are actionable, and to the same paradigm principle:

@thierrykoblentz In user testing of web-based apps, strong preference to have same paradigm, for when interacting with sighted colleagues.

— Sarah Bourne (@sarahebourne) May 21, 2016

Even though it may worth ignoring that principle sometime:

@thierrykoblentz I thought it would be confusing, but not a single complaint. It has challenged my convictions.

— Sarah Bourne (@sarahebourne) May 21, 2016

I’ve always challenged this idea of serving the same experience no matter what; because I think one cannot succeed in offering the best experience to a wide variety of users across different scenarios while strictly obeying that principle. It just can’t work. Also, because the display of components on the web may vary depending on many factors: desktop, mobile, screen size, device orientation, browsers, login state, etc.

There Is Nothing We Can Do

Unfortunately, we cannot really detect SR users to cater for them by disabling the script. And I don’t think this is going to change anytime soon because of various concerns.

There are many valid points in that piece from Leonie Watson, but there is one I don’t agree with:

Most things that make a website usable with a screen reader are achieved by conforming to web standards, and the rest require relatively little modification. In these days of responsive design, including a media feature for screen readers would automatically double the work involved. You’d need to serve up a screen reader alternative for every break point version of your website.

In the case of these widgets, there would be no extra work involved because we could achieve true progressive enhancement by leveraging the “very first layer”, from which we get the semantics that provides a very basic/simple mental model.

So in the absence of a straight mechanism to “dumb down” the widget for SR users, we could offer a “kill switch” in the form of:

<div class="widget" 
     aria-label="Use 'Ctrl + T' to toggle the interface of this section (from a Tab Panel widget to a simple succession of headings and their respective sections)>

Final Thoughts

If you think you have a valid case for a Tab Panel that uses jump links, then you may be thinking of Nav Tabs, for example:

[1] 10 years ago, that was me asking that question #1 to Jakob Nielsen :)