aria-tablist

Aria Tablist

npm version gzip size

Dependency-free plain JavaScript module for WCAG compliant tablists. Also great for accordions.

Try out the examples

Key design goals and features are:

Installation / usage

Grab from NPM and use in a module system:

npm install aria-tablist
import AriaTablist from 'aria-tablist';
new AriaTablist(document.getElementById('tablist'));

Or grab the minified JavaScript from unpkg:

<script src="https://unpkg.com/aria-tablist/dist/aria-tablist.min.js"></script>

The module relies entirely on standard attributes: it sets the role on elements if it needs to, aria- attributes for indicating behaviour to screen readers, and relies on setting and removing hidden="hidden" to toggle element visibility. This means that you can use all of your own class names and styling, and the module won’t override them.

HTML Requirements / Progressive Enhancement

When the module is called on an element, the following steps are taken:

  1. The module will look for elements with role="tab" set.
  2. If none are found, all direct children will be processed.
  3. For each assumed tab, the module will check for a matching tabpanel by:
    1. Checking for an aria-controls attribute on the tab, and searching for an element with a matching id.
    2. If the tab has an id, searching for an element with an aria-labelledby attribute that matches that id.
  4. For any tabs that were processed where a matching panel was not found, if they had role="tab" set, the role attribute will be removed to prevent confusion to screen reader users.
  5. The found tabs and associated panels will then have the relevant role and aria- attributes set automatically.

This means your HTML only needs to indicate the relationship between the tabs and panels, and the module will handle the rest:

<div id="tabs">
    <div id="tab-1">Panel 1</div>
    <div id="tab-2">Panel 2</div>
    <div id="tab-3">Panel 3</div>
</div>

<div aria-labelledby="tab-1">...</div>
<div aria-labelledby="tab-2">...</div>
<div aria-labelledby="tab-3">...</div>

<script>
    new AriaTablist(document.getElementById('tabs'));
</script>

So if you need to cater for users without JavaScript, or if the JavaScript fails to load for whatever reason, there won’t be any applicable roles set that would confuse a screen reader user.

You can of course include all of the optimal ARIA attributes straight away if you wish, including indicating which tab should be active by default:

<div id="tabs" role="tablist" aria-label="Tabs">
    <div role="tab" tabindex="-1" aria-controls="panel-1" id="tab-1">
        Panel 1
    </div>
    <div role="tab" tabindex="0" aria-selected="true" aria-controls="panel-2" id="tab-2">
        Panel 2
    </div>
    <div role="tab" tabindex="-1" aria-controls="panel-3" id="tab-3">
        Panel 3
    </div>
</div>

<div role="tabpanel" aria-labelledby="tab-1" hidden="hidden" id="panel-1">...</div>
<div role="tabpanel" aria-labelledby="tab-2" id="panel-2">...</div>
<div role="tabpanel" aria-labelledby="tab-3" hidden="hidden" id="panel-3">...</div>

Options

Most of the functionality is assumed from the included ARIA attributes in your HTML (see the examples). The remaining available options and their defaults are:

{
    /**
     * @description delay in milliseconds before showing tab(s) from user interaction
     */
    delay: 0,

    /**
     * @description allow tab deletion - can be overridden per tab by setting data-deletable="false"
     */
    deletable: false,

    /**
     * @description make all tabs focusable in the page's tabbing order (by setting a `tabindex` on them), instead of just 1
     */
    focusableTabs: false,

    /**
     * @description make all tab panels focusable in the page's tabbing order (by setting a `tabindex` on them)
     */
    focusablePanels: true,

    /**
     * @description activate a tab when it receives focus from using the arrow keys
     */
    arrowActivation: false,

    /**
     * @description value to use when setting tabs or panels to be part of the page's tabbing order
     */
    tabindex: 0,

    /**
     * @description callback each time a tab opens
     */
    onOpen: (panel, tab) => {},

    /**
     * @description callback each time a tab closes
     */
    onClose: (panel, tab) => {},

    /**
     * @description callback when a tab is deleted
     */
    onDelete: (tab) => {},

    /**
     * @description callback once ready
     */
    onReady: (tablist) => {}
}

All component options that accept a Function will have their context (this) set to include the full autocomplete API (assuming you use a normal function: () {} declaration for the callbacks instead of arrow functions).

API

The returned AriaTablist class instance exposes the following API (which is also available on the original element’s ariaTablist property):

{
    /**
     * @description the tab elements the module currently recognises
     */
    tabs: Element[];

    /**
     * @description the panel elements the module currently recognises
     */
    panels: Element[];

    /**
     * @description the current options object
     */
    options: Object;

    /**
     * @description trigger a particular tab to open (even if disabled)
     * @param {Number|Element} index - tab index, or tab element
     * @param {Boolean} [focusTab=true] - move focus to the tab before opening
     */
    open(index: Number|Element, focusTab: Boolean = true): void;

    /**
     * @description trigger a particular tab to close (even if disabled)
     * @param {Number|Element} index - tab index, or tab element
     * @param {Boolean} [focusTab=false] - move focus to the tab before closing
     */
    close(index: Number|Element, focusTab: Boolean = false): void;

    /**
     * @description delete a particular tab and its corresponding panel (if deletable)
     * @param {Number|Element} index - tab index, or tab element
     */
    delete(index: Number|Element): void;

    /**
     * @description destroy the module - does not remove the elements from the DOM
     */
    destroy(): void;
}