eBay MIND Patterns
  • Introduction
  • Messaging
    • Alert Dialog
    • Confirm Dialog
    • File Preview Card
    • Form Validation
    • Inline Notice
    • Input Meter
    • Input Validation
    • Page Notice
    • Star Rating (static)
    • Time
    • Toast Dialog
    • Tourtip
  • Input
    • Button
    • Checkbox
    • Chips Combobox
    • Combobox
    • Date Picker
    • File Input
    • Input Dialog
    • Listbox
    • Listbox Button
    • Menu
    • Menu Button
    • Phone Input
    • Radio
    • Select
    • Star Rating (interactive)
    • Switch
    • Toggle Button
    • Toggle Button Group
  • Navigation
    • Breadcrumbs
    • Fake Menu Button
    • Fake Tabs
    • Link
    • Pagination
    • Skip Navigation
    • Tile
  • Disclosure
    • Accordion
    • Carousel
    • Lightbox Dialog
    • Details
    • Flyout
    • Footnote
    • Infotip Button
    • Panel Dialog
    • Pulldown List
    • Segmented Buttons
    • Tabs
    • Tooltip
  • Structure
    • Description List
    • Form
    • Heading
    • Image
    • Item Tile
    • Layout Grid
    • Region
    • Table
    • Table Cell
  • Techniques
    • Active Descendant
    • Ambiguous Label
    • Background Icon
    • Keyboard Trap
    • Live Region
    • Offscreen Text
    • Roving Tabindex
    • Skip to Main Content
    • Alternative Text
  • Anti-Patterns
    • Disabling Pinch-to-Zoom
    • Hand Cursor on Buttons
    • JavaScript HREF
    • Layout Table
    • Mouse Hover on Static Elements
    • Open New Window
    • Setting Focus on Page Load
    • Tabindex-itis
    • Title Tooltip
  • Appendix
    • ARIA Essentials
    • Checklist
    • FAQ
    • Keyboard Interface
    • Known Issues
    • Legacy Patterns
      • Fullscreen Dialog
    • MIND Pattern Template
    • Pattern Naming Scheme
    • References
    • Utilities
Powered by GitBook
On this page
  • Introduction
  • Working Examples
  • Terminology
  • Configuration
  • Best Practices
  • Interaction Design
  • Developer Guide
  • Utilities
  • References
  1. Disclosure

Tabs

Hide and disclose panels of content via a series of interactive tabs.

PreviousSegmented ButtonsNextTooltip

Last updated 3 months ago

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 tabs follow the principal of progressive disclosure.

Working Examples

Terminology

  • tabs: the composite patterns as a whole, containing a tablist, tabs and tabpanels

  • tabs heading: the heading that immediately precedes the tabs widget

  • 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

  • tab heading: the offscreen heading that maintains correct heading structure

Configuration

  • autoSelect: for keyboard users, tab selection can either follow keyboard focus (known as auto selection), or require an additional ENTER or SPACEBAR press to set selection (known as manual selection).

Best Practices

Tab list 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.

Tab list must have exactly one selected tab.

If all tab panel content is rendered on page load, tabs should be configured with autoSelect enabled.

If all tab panel content is rendered lazily on client (i.e. using AJAX call), tabs should be configured with autoSelect turned off.

Interaction Design

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

Keyboard

For tabs with autoSelect enabled, ARROW keys move keyboard focus to next/previous tab and also select that tab (i.e. aria-selected="true").

For tabs without autoSelect enabled, ARROW keys move keyboard focus to next/previous tab, but ENTER or SPACEBAR key is required to set the tab to a selected state.

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

If tab panel does not contain focusable element(s), TAB key on selected 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.

Developer Guide

Our first example implementation will create a tabs widget with autoSelect configuration enabled. All of the tab panel content will be rendered to the DOM on server side load.

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.

For a tabs widget where content is not rendered on first server side load, using this progressive enhancement type approach is not as applicable.

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.

Links

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>

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).

NOTE: we have found that in some browsers, activating a same page link will only scroll the browser to the target, but the focus is left behind on the link. Adding tabindex="-1" also helps move and set focus on the target element.

Checkpoint

That's it! Our content is available and accessible 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)

/* 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 our JavaScript 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:

tabs.js
module.exports = class {
    constructor(widgetEl) {
        this._el = widgetEl;

        const tabList = this._el.querySelector('.tabs__items', this._el);
        const tabs = Util.querySelectorAllToArray('.tabs__item', this._el);
        const panels = Util.querySelectorAllToArray('.tabs__panel', this._el);
        const links = Util.querySelectorAllToArray('a', tabList);
    }
}

ARIA Roles

How does a screen reader know this is a tabs widget? We must add ARIA roles to the tab list, tabs, and panels.

tabs.js
tabList.setAttribute('role', 'tablist');
tabs.forEach(el => el.setAttribute('role', 'tab'));
panels.forEach(el => el.setAttribute('role', 'tabpanel'));

Remove Link Behaviour

We currently have links nested inside of our tab elements. To avoid conflicts with our tabs we must remove any semantics and behaviour, effectively turning them into span tags.

tabs.js
function removeLink(el) {
    el.setAttribute('role', 'presentation');
    el.removeAttribute('href');
}

ARIA States

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

tabs.js
let initialIndex = this._options.initialIndex;

tabs[initialIndex].setAttribute('aria-selected', 'true');

// hide all unselected panels
panels.filter((el, i) => i !== initialIndex).forEach(el => el.hidden = true);

ARIA Properties

How does a screen reader know which panel belongs to which tab, and the label of each panel? We must add aria-controls and aria-labelledby properties.

tabs.js
function linkTabToPanel(widgetID, el, i) {
    el.setAttribute('id', widgetID + '-tab-' + i);
    el.setAttribute('aria-controls', widgetID + '-panel-' + i);
}

function linkPanelToTab(widgetID, el, i) {
    el.setAttribute('id', widgetID + '-panel-' + i);
    el.setAttribute('aria-labelledby', widgetID + '-tab-' + i);
}

Roving Tabindex

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" tab. When a user tabs away from the widget and then back again, focus will return to this "selected" tab.

State Management

When the roving tabindex changes, we must update the aria-selected and hidden states.

tabs.js
this._el.addEventListener('rovingTabindexChange', function (e) {
    tabs[e.detail.fromIndex].setAttribute('aria-selected', 'false');
    panels[e.detail.fromIndex].hidden = true;

    tabs[e.detail.toIndex].setAttribute('aria-selected', 'true');
    panels[e.detail.toIndex].hidden = false;
});

Prevent Page Scroll

tabs.js
const ScrollKeyPreventer = require('makeup-prevent-scroll-keys');

ScrollKeyPreventer.add(tabList);

Widget Init

Finally we can mark our widget as initialised. Now our CSS rules for our progressively enhanced widget will kick in.

tabs.js
this._el.classList.add('tabs--js');

Final Checkpoint

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

Utilities

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

References

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 section below for more details.

Experience the tab pattern in action on our .

Examine the required markup structure by viewing our .

View a fully-style example on our site.

To maintain correct heading structure, tab panels should contain an heading. The level of this panel heading must be exactly one level lower than the heading preceding the tablist. The heading text must match the corresponding tab text.

Only one tab can be keyboard focusable at any time. This is known as a .

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

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

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:

Our selectors are based on our .

This behaviour is known as a . We provide a sample module for you to reference.

When the selected tab has focus, we must prevent arrow keys and spacebar from scrolling the page. We provide another module, , to make this trivial.

- Useful for implementing the arrow key behaviour to change tabs

- Useful for preventing keys from scrolling page while focus is on a widget

fake tabs
examples site
bones site
eBay Skin
offscreen
roving tab index
Progressive Enhancement
bones
FOUC
bones markup convention
roving tabindex
makeup-roving-tabindex
makeup-prevent-scroll-keys
makeup-roving-tabindex
makeup-prevent-scroll-keys
WAI-ARIA Authoring Practices 1.1: Tabs
Shipping option tabs