eBay

Tab

Shipping option tabs

Introduction

A tab is a control that allows the user to select and display a single panel of content from a group of choices. By decluttering the user-interface in this way, we say that a tab follows the principals of progressive disclosure.

Selecting a tab should update the visible panel without a full page reload. If a full page load is required instead (i.e. acting like a link), please see the fake tabs section below for more details.


Last Updated June 10th, 2017

Replaced aria-hidden with hidden.


Working Examples

You can take a look at the tab pattern in action on our examples site.

You can get an idea of the required markup structure by viewing our bones site.

For a real life example, you can also see the tab pattern in action on our eBay Skin site.


Terminology

tabs
the composite patterns as a whole, containing a tablist, tabs and tabpanels
tab list
contains two or more tabs
tab
a type of button that displays it's associated tabpanel
selected tab
the currently selected tab
tab panel
contains the content related to the tab

Best Practices

Tablist must be preceded by a heading. All tabs must be thematically related to this heading. For example, a set of 'Shipping Services' tabs might contain a tab each for USPS, FedEx and UPS.

Tablist must have exactly one selected tab.

If following progressive enhancement, tab panels must contain an offscreen heading. The heading text is the same as the corresponding tab text. The level of this panel heading must be one level lower than the heading preceding the tablist.


Interaction Design

This section provides guidance for keyboard, screen reader and pointing devices.

Keyboard

Selected tab must be focusable.

When focus is on selected tab, RIGHT/LEFT ARROW changes selected tab to next/previous tab in tab list and focus moves to that tab.

If tab panel contains focusable element(s), TAB key on active tab must move focus to first focusable element in tab panel.

If tab panel does not contain focusable element(s), TAB key on active-tab must move focus to next focusable element on page.

Screen Reader

Tab must be announce as "Tab".

Tab label must be announced, for example "Select Shipping for me".

Tab selected state must be announced.

Virtual cursor navigation can move from tab to tab without changing the active tab selection. The invoke command (e.g. VO+SPACE on Voiceover) selects the tab under the virtual cursor.

Developer Guide

Our sample implementation follows the Progressive Enhancement strategy; we build in a layered fashion that allows everyone to access the basic content and functionality of a web page.

The three layers are:

  1. Content (HTML)
  2. Presentation (CSS)
  3. Behaviour (JS)

The tabs and their related content elements can be fully visible and accessible without CSS and JavaScript as simple hyperlinks and page anchors respectively.

Content (HTML)

The goal of our content layer is to add all of our tabs and their respective panel content to the page.

For the purposes of this example, all panel content will be rendered server-side. You may wish to consider lazy-loading the content of each panel with AJAX. If you do utilise lazy-loading, be aware that your content will not be available in a non-JavaScript scenario.

The tabs begin life as simple same-page navigation links, linking to the content anchors (panels) below it on the same page:

<div class="tabs tabs--horizontal">
    <h2>My eBay</h2>
    <ul class="tabs__items">
        <li class="tabs__item"><a href="#buying">Buying</a></li>
        <li class="tabs__item"><a href="#bought">Bought</a></li>
        <li class="tabs__item"><a href="#selling">Selling</a></li>
        <li class="tabs__item"><a href="#sold">Sold</a></li>
    </ul>
    <div class="tabs__content">
        <div class="tabs__panel" id="buying" tabindex="-1">
            <h3>...</h3>
            <p>...</p>
        </div>
        <div class="tabs__panel" id="bought" tabindex="-1">
            <h3>...</h3>
            <p>...</p>
        </div>
        <div class="tabs__panel" id="selling" tabindex="-1">
            <h3>...</h3>
            <p>...</p>
        </div>
        <div class="tabs__panel" id="sold" tabindex="-1">
            <h3>...</h3>
            <p>...</p>
        </div>
    </div>
</div>

We call this markup structure our bones; our CSS and JavaScript will be expecting this exact DOM structure convention.

This structure has been chosen carefully. It allows us to display tabs horizontally and vertically simply by changing the second class (to tabs--horizontal or tabs--vertical).

Checkpoint

That's it! Our content is available to anyone in a non-CSS and non-JS state.

Presentation (CSS)

The goal of our presentation layer is to style the links to look like folder style tabs.

How you choose to style the links is outside the scope of this document, because every website likes to make their tabs look slightly different!

Flash of Unstyled Content (FOUC)

FOUC may occur before JavaScript initialises the widget, i.e. all panel content may be visible briefly. One way to alleviate this is to set a fixed height on the tab panel container:

/* before js init */
.tabs__content {
    height: 150px;
    overflow-y: auto;
}
/* after js init */
.tabs--js .tabs__content {
    height: auto;
}

We have chosen an arbitrary value of 150px for our example. After JS initialises the widget, it's height will grow or shrink to match the content of the currently selected panel. Of course if fixed height is what you desire, then you can leave the fixed value in place.

Checkpoint

Our tabs now appear visually like tabs, and the panel content is still fully operable without JavaScript (albeit with ugly vertical scrollbars).

Behaviour (JS)

The goal of our JavaScript is to implement our interaction design.

Plugin Boilerplate

We start by caching references to our most important elements:

(function ( $ ) {
    $.fn.tabs = function tabs () {
        return this.each(function onEach() {
            var $tabsWidget = $(this),
                $tablist = $tabsWidget.find('.tabs__items'),
                $tabs = $tablist.find('.tabs__item'),
                $links = $tablist.find('a'),
                $panelcontainer = $tabsWidget.find('.tabs__content'),
                $panels = $panelcontainer.find('.tabs__panel');

            // implementation goes here
        });
    };
}( jQuery ));

Our selectors are based on our bones markup convention.

Widget Init

Let's mark our widget as initialised:

$tabsWidget.addClass('tabs--js');

Now our CSS rules for our progresively enhanced widget will kick in.

ARIA Roles

How does a screen reader know this is a tabs widget? We must add ARIA roles.

Roles only need to be added once:

$tablist.attr('role', 'tablist');
$tabs.attr('role', 'tab')
$panels.attr('role', 'tabpanel')
$links.attr('role', 'presentation').removeAttr('href');

Notice the last statement, which essentially now turns our old same-page links into meaningless span elements, so that they don't conflict with our tabs.

ARIA States

How does a screen reader know which tab is currently selected and which panel is visible? We must add ARIA states.

States must be monitored and then bound to any changes in the view:

$tabs
    .attr('aria-selected', 'false')
    .first()
        .attr('tabindex', '0')
        .attr('aria-selected', 'true');
$panels
    .prop('hidden', true)
    .first()
        .prop('hidden', false);

By default we make the first tab the selected tab.

ARIA Properties

How does a screen reader know which panel belongs to which tab? We must add ARIA properties.

Properties are usually just added once and then left alone:

$tabs.each(function onEachTab(idx, tabEl) {
    var $tab = $(tabEl),
        tabId = $tabsWidget.attr('id') + '_tab_' + idx,
        panelId = $tabsWidget.attr('id') + '_panel_' + idx;

    $tab
        .attr('id', tabId)
        .attr('aria-controls', panelId);

    $panels.eq(idx)
        .attr('id', panelId)
        .attr('aria-labelledby', tabId);
});

All panels are now labelled and controlled by their respective tab.

Roving Tab Index

If there are many tabs it would require many TAB key presses to navigate past the widget, therefore tabs should be navigated with ARROW keys instead.

Only one tab can be focussable at any given time. This is always the selected/active tab, so that when a user tabs away from the widget and then back again, focus will return to the active tab.

This behaviour is known as a roving tabindex. We provide a sample jquery-roving-tabindex plugin for you to reference.

Prevent Page Scroll

When tab has focus, we must prevent arrow keys and spacebar from scrolling the page. jQuery event delegation make this trivial:

$tablist.on('keydown', '[role=tab]', function(e) {
    var keyCode = e.keyCode;
    if (keyCode === 32 || keyCode === 38 || keyCode === 40) {
        e.preventDefault();
    }
});

Notice the 2nd parameter which scopes the keydown event to a particular selector/element. Very handy!

Final Checkpoint

We have enhanced our markup with ARIA roles, states and properties for screen reader users, and implemented keyboard behaviour.

Useful Plugins

We have some experimental jQuery plugins that may assist you with creation of an accessible tabs widget:

References

results matching ""

    No results matching ""