Aria Tablist
Dependency-free plain JavaScript module for WCAG compliant tablists. Also great for accordions.
Key design goals and features are:
- multi and single select modes
- horizontal and vertical modes: Adjusts arrow key usage for moving focus between tabs
- progressive enhancement: Allows for only the tab and panel relationship to be indicated in the DOM, and adds
role
andaria
attributes automatically as needed - accessibility: Follows the WCAG spec by default, with options to tweak behaviour
- compatibility: Broad browser and device support (IE9+)
- starting states: Can use
aria-selected="true"
ordata-selected="true"
to indicate which tab(s) should be enabled by default. - deletion: Can enable tab (and panel) deletion using the delete key
Installation / usage
Grab from NPM and use in a module system:
npm install aria-tablist
import AriaTablist from 'aria-tablist';
AriaTablist(document.getElementById('tablist'), options);
Or grab the minified JavaScript from unpkg:
<script src="https://unpkg.com/aria-tablist"></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:
- The module will search for
tab
elements using thetabSelector
option ('[role="tab"]'
by default). - If none are found, all direct children will be processed.
- For each assumed
tab
, the module will check for a matchingtabpanel
by:- Checking for an
aria-controls
ordata-controls
attribute on thetab
, and searching for an element with a matchingid
. - If the
tab
has anid
, searching for an element with anaria-labelledby
ordata-labelledby
attribute that matches thatid
.
- Checking for an
- For any tabs that were processed where a matching panel was not found, if they had
role="tab"
set, therole
attribute will be removed to prevent confusion to screen reader users. - The found tabs and associated panels will then have the relevant
role
andaria-
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 data-labelledby="tab-1">...</div>
<div data-labelledby="tab-2">...</div>
<div data-labelledby="tab-3">...</div>
<script>
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="Fruits">
<div role="tab" tabindex="-1" aria-controls="panel-1" id="tab-1">
Apple
</div>
<div role="tab" tabindex="0" aria-selected="true" aria-controls="panel-2" id="tab-2">
Orange
</div>
<div role="tab" tabindex="-1" aria-controls="panel-3" id="tab-3">
Pear
</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:
{
/**
* delay in milliseconds before showing tab(s) from user interaction
*/
delay: number = 0;
/**
* allow tab deletion via the keyboard - can be overridden per tab by setting `data-deletable="false"`
*/
deletable: boolean = false;
/**
* make all tabs focusable in the page's tabbing order (by setting a `tabindex` on them), instead of just 1
*/
focusableTabs: boolean = false;
/**
* make all tab panels focusable in the page's tabbing order (by setting a `tabindex` on them)
*/
focusablePanels: boolean = true;
/**
* activate a tab when it receives focus from using the arrow keys
*/
arrowActivation: boolean = false;
/**
* enable all arrow keys for moving focus, instead of horizontal or vertical arrows based on `aria-orientation` attribute
* (left and up for previous, right and down for next)
*/
allArrows: boolean = false;
/**
* the selector to use when initially searching for tab elements;
* if none are found, all direct children of the main element will be processed
*/
tabSelector: string = '[role="tab"]';
/**
* value to use when setting tabs or panels to be part of the page's tabbing order
*/
tabindex: number | string = 0;
/**
* callback each time a tab opens
*/
onOpen: (panel: HTMLElement, tab: HTMLElement) => void;
/**
* callback each time a tab closes
*/
onClose: (panel: HTMLElement, tab: HTMLElement) => void;
/**
* callback when a tab is deleted
*/
onDelete: (tab: HTMLElement) => void;
/**
* callback once ready
*/
onReady: (tablist: HTMLElement) => void;
}
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):
{
/**
* the tab elements the module currently recognises
*/
tabs: HTMLElement[];
/**
* the panel elements the module currently recognises
*/
panels: HTMLElement[];
/**
* the current options object
*/
options: AriaTablistOptions;
/**
* trigger a particular tab to open (even if disabled)
*/
open(index: number | HTMLElement, focusTab: boolean = true): void;
/**
* trigger a particular tab to close (even if disabled)
*/
close(index: number | HTMLElement, focusTab: boolean = false): void;
/**
* delete a particular tab and its corresponding panel (if deletable)
*/
delete(index: number | HTMLElement): void;
/**
* destroy the module - does not remove the elements from the DOM
*/
destroy(): void;
}