Screen-reader friendly navigation tabs
A vanilla JavaScript version of Heydon Pickerings Simple ARIA tab interface with some slight modifications.
A Codepen demo is available. As used on the Tesco Easter 2016 hub (Opening hours results).
Navigation tabs example
For demo purposes a white focus outline is applied to clearly indicate keyboard actions. Amend to taste.
Section heading A
Section content A here Example focusable link A
Section heading B
Section content B here without a focusable link
Section heading C
Section content C here Example focusable link C
Use the tab key to jump link to visible link, and the arrow keys to navigate across the tabs.
Optionally allows mouse hover to activate the tab via the class "tl-hoverable" applied to the ul.
On-off states are controlled via CSS using ARIA properties.
Marking up the tabs
The tabs are an unordered list of links with a class of "tl_links" ("tl" represents "tablist"). Nothing special with an "tl_tab-on" class for initial state. The "tl-hoverable" class is optional and experimental.
<ul class="tl_list tl-hoverable">
<li><a href="#A">Section A</a></li>
<li><a href="#B" class="tl_tab-on">Section B</a></li>
<li><a href="#C">Section C</a></li>
</ul>
Sections of content, one per tab. Only the ID is important as it ties the tab link to the content section.
<section id=A class=tl_section>
<h3>Heading A</h3>
<p>Content A</p>
</section>
<section id=B class=tl_section>
<h3>Heading B</h3>
<p>Content B</p>
</section>
<section id=C class=tl_section>
<h3>Heading C</h3>
<p>Content C</p>
</section>
Styling the tabs
Only one line of the CSS is required. The rest is just layout enhancement.
Uses flexbox to layout the tabs side-by-side.
.tl_list {
margin: 0;
padding: 0;
list-style: none;
display: flex;
align-items: center;
justify-content: left;
}
.tl_list li {
margin: 0;
padding: 0;
width: 33.3333%;
text-align: center;
}
.tl_list a {
padding: 1rem 0.5rem;
display: block;
text-decoration: none;
}
The hidden sections:
.tl_section {
padding: 1rem;
min-height: 3rem;
}
/* Lose any dangling margins from the content */
.tl_section > :last-child {
margin-bottom: 0;
}
/* Colour the selected tab and the visible section the same */
[role="tablist"] [aria-selected="true"],
[role="tabpanel"] {
color: #007471;
background-color: #D9E4F6;
}
Remove hidden sections from the keychain.
This is the only CSS rule actually required.
[role="tabpanel"][aria-hidden="true"] {
display: none;
}
Scripting
JavaScript is required for the tabs to function as expected so therefore the ARIA roles and regions are added by the JavaScript.
var accessibleTabs6 = (function () {
"use strict";
var d = document;
var tabListClass;
var tabs;
var onClass;
var useHover;
Tab switching is done via the ARIA controls.
var _setTab = function (tab, switchOn) {
d.getElementById(tab.panelId).setAttribute("aria-hidden", !switchOn);
tab.setAttribute("aria-selected", switchOn);
tab.setAttribute("tabindex", switchOn ? "0" : "-1");
};
var _setTabsOff = function () {
var i = tabs.length;
while (i--) {
_setTab(tabs[i], false);
}
};
var _activateTab = function (e) {
var tab = e.target;
e.preventDefault();
_setTabsOff();
_setTab(tab, true);
d.getElementById(tab.panelId).children[0].focus();
};
Allow the arrow left & right keys to select a different tab.
var _keypressed = function (e) {
var tab = e.target;
var newNo = -1;
var maxNo = tabs.length - 1;
if (e.keyCode === 37) { // left arrow
newNo = (tab.no === 0) ? maxNo : tab.no - 1;
}
if (e.keyCode === 39) { // right arrow
newNo = (tab.no === maxNo) ? 0 : tab.no + 1;
}
if (newNo > -1) {
_setTabsOff();
_setTab(tabs[newNo], true);
tabs[newNo].focus();
}
};
Event listeners for click and keypress. Optionally adding hover and focus.
var _events = function (tab) {
tab.addEventListener("click", _activateTab, false);
tab.addEventListener("keydown", _keypressed, false);
if (useHover) {
tab.addEventListener("mouseover", _activateTab, false);
}
};
Initialise the tab and panel ARIA attributes.
var _initialiseAriaAttributes = function (tab) {
var tabPanel = d.getElementById(tab.panelId);
tab.parentNode.setAttribute("role", "presentation");
tab.setAttribute("role", "tab");
tab.setAttribute("aria-controls", tab.panelId);
tabPanel.setAttribute("role", "tabpanel");
tabPanel.setAttribute("aria-labelledby", tab.id);
// Make first section object keyboard focusable (
// preferably a heading
tabPanel.children[0].setAttribute("tabindex", "0");
};
Read the class configuration and apply to closure variables.
var _setUpConfig = function (cfg) {
tabListClass = cfg.tabListClass || "tl_list";
onClass = cfg.onClass || "tl_tab-on";
useHover = tabList.classList.contains(cfg.hoverableClass || "tl-hoverable");
};
Initialise tab values.
var _setUpTab = function (tab, panelId, count) {
tab.no = count;
tab.id = "tab-" + panelId;
tab.panelId = panelId;
_initialiseAriaAttributes(tab);
_events(tab);
};
Initialise accessible tabs.
var _initialiseTabList = function (tabList) {
var defaultTab = 0;
var panelId;
var tabPanel;
var i;
if (tabList) {
tabList.setAttribute("role", "tablist");
tabs = tabList.getElementsByTagName("a");
i = tabs.length;
while (i--) {
panelId = tabs[i].href.slice(tabs[i].href.lastIndexOf("#") + 1);
tabPanel = d.getElementById(panelId);
if (tabPanel) {
_setUpTab(tabs[i], panelId, i);
if (tabs[i].classList.contains(onClass)) {
defaultTab = i;
// onClass only used to declare intial state, so remove from DOM
tabs[i].classList.remove(onClass);
d.getElementById(panelId).classList.remove(onClass);
}
}
}
_setTabsOff();
_setTab(tabs[defaultTab], true);
}
};
Check browser support level.
var _isMustardCut = function () {
return (
(typeof d.querySelectorAll === "function") &&
d.addEventListener &&
!!d.documentElement.classList
);
};
Find and run through each tablist.
var init = function (cfg) {
var tabLists;
var i;
if (_isMustardCut()) {
_setUpConfig(cfg);
tabLists = d.getElementsByClassName(tabListClass);
i = tabLists.length;
while (i--) {
_initialiseTabList(tabLists[i]);
}
}
};
Expose the initialise function to the outside world.
return {
init: init
};
}());
Run with optional class names over rides.
accessibleTabs6.init({
tabListClass : "tl_list", // default, may omit
onClass : "tl_tab-on", // default, may omit
hoverableClass : "tl-hoverable" // default, may omit
});
Social links and email client: