Tabs provide the ability to navigate different views or facets of the same content. For example, on a page listing your department’s faculty, you might show leadership, tenured, and adjunct instructors in different views. Rather than listing those views in your navigation, you could divide them into tabs.
Tabs can represent static pages or dynamic views of one page, with a single or multiple URL for each view.
role="tablist"
and an aria-label
.<button>
element for tabs with ARIA role="tab"
and aria-selected="true"
or "false"
.<button>
element if tabs change the content view within the same page.<a>
element if tabs link to other pages with a unique URL.Further Readings
<!-- .tabs is the container for both tabs and panels -->
<div class="tabs">
<!-- .tablist is the container for tabs only -->
<div role="tablist" aria-label="content-tabs">
<button role="tab"
aria-selected="true"
aria-controls="panel-01-tab"
id="panel-01"
class="btn tablinks">
Leadership
</button>
<button role="tab"
aria-selected="false"
aria-controls="panel-02-tab"
id="panel-02"
class="btn tablinks"
tabindex="-1">
Tenured
</button>
<button role="tab"
aria-selected="false"
aria-controls="panel-03-tab"
id="panel-03"
class="btn tablinks"
tabindex="-1"
data-deletable="">
Adjunct
</button>
</div>
<!-- .tabcontent contain panels of content -->
<div tabindex="0"
role="tabpanel"
id="panel-01-tab"
aria-labelledby="panel-01"
class="tabcontent">
<p>Panel 1: Leadership</p>
<p>Include content about your department's faculty leadership here.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sem neque, pulvinar ac bibendum eget, hendrerit a dolor. Nulla nec ex nulla.</p>
<p>
<a href="#">Example of inline link</a>.
</p>
</div>
<div tabindex="0"
role="tabpanel"
id="panel-02-tab"
aria-labelledby="panel-02"
hidden=""
class="tabcontent">
<p>Panel 2: Tenured</p>
<p>Include content about your department's tenured faculty here.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sem neque, pulvinar ac bibendum eget, hendrerit a dolor. Nulla nec ex nulla.</p>
<p>
<a href="#">Example of inline link</a>.
</p>
</div>
<div tabindex="0"
role="tabpanel"
id="panel-03-tab"
aria-labelledby="panel-03"
hidden=""
class="tabcontent">
<p>Panel 3: Adjunct</p>
<p>Include content about your department's adjunct faculty here.</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse sem neque, pulvinar ac bibendum eget, hendrerit a dolor. Nulla nec ex nulla.</p>
<p>
<a href="#">Example of inline link</a>.
</p>
</div>
</div>
The following script is required for the tabs component to work. Find the file at /src/js/tabs.js
.
The script does the following:
/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*/
(function () {
let tablist = document.querySelectorAll('[role="tablist"]')[0];
let tabs;
let panels;
let delay = determineDelay();
// Exit the script if tablist is not defined (on any page where exists no tablist)
if (!tablist) {
return;
}
// Set overflow styles
setOverflowStyles(tablist);
generateArrays();
function generateArrays () {
tabs = document.querySelectorAll('[role="tab"]');
panels = document.querySelectorAll('[role="tabpanel"]');
};
// For easy reference
let keys = {
end: 35,
home: 36,
left: 37,
up: 38,
right: 39,
down: 40,
delete: 46
};
// Add or subtract depending on key pressed
let direction = {
37: -1,
38: -1,
39: 1,
40: 1
};
// Bind listeners
for (i = 0; i < tabs.length; ++i) {
addListeners(i);
};
// Check if tabs overflow max-width of container
function isTabListOverflow (tablist) {
let buttonWidth = 0;
let buttons = tablist.querySelectorAll('[role="tab"]');
for (let idx = 0; idx < buttons.length; idx++) {
buttonWidth += buttons[idx].offsetWidth;
}
return buttonWidth > tablist.offsetWidth;
}
function setOverflowStyles (tablist) {
if (isTabListOverflow(tablist)) {
// tablist.style['overflow'] = 'scroll';
for (let i = 0; i < tablist.children.length; i++) {
tablist.children[i].style['width'] = '25%';
tablist.children[i].style['white-space'] = 'normal';
tablist.children[i].style['vertical-align'] = 'bottom';
}
}
}
function addListeners (index) {
tabs[index].addEventListener('click', clickEventListener);
tabs[index].addEventListener('keydown', keydownEventListener);
tabs[index].addEventListener('keyup', keyupEventListener);
// Build an array with all tabs (<button>s) in it
tabs[index].index = index;
};
// When a tab is clicked, activateTab is fired to activate it
function clickEventListener (event) {
let tab = event.target;
activateTab(tab, false);
};
// Handle keydown on tabs
function keydownEventListener (event) {
let key = event.keyCode;
switch (key) {
case keys.end:
event.preventDefault();
// Activate last tab
activateTab(tabs[tabs.length - 1]);
break;
case keys.home:
event.preventDefault();
// Activate first tab
activateTab(tabs[0]);
break;
// Up and down are in keydown
// because we need to prevent page scroll >:)
case keys.up:
case keys.down:
determineOrientation(event);
break;
};
};
// Handle keyup on tabs
function keyupEventListener (event) {
let key = event.keyCode;
switch (key) {
case keys.left:
case keys.right:
determineOrientation(event);
break;
case keys.delete:
determineDeletable(event);
break;
};
};
// When a tablist's aria-orientation is set to vertical,
// only up and down arrow should function.
// In all other cases only left and right arrow function.
function determineOrientation (event) {
let key = event.keyCode;
let vertical = tablist.getAttribute('aria-orientation') === 'vertical';
let proceed = false;
if (vertical) {
if (key === keys.up || key === keys.down) {
event.preventDefault();
proceed = true;
};
}
else {
if (key === keys.left || key === keys.right) {
proceed = true;
};
};
if (proceed) {
switchTabOnArrowPress(event);
};
};
// Either focus the next, previous, first, or last tab
// depending on key pressed
function switchTabOnArrowPress (event) {
let pressed = event.keyCode;
for (x = 0; x < tabs.length; x++) {
tabs[x].addEventListener('focus', focusEventHandler);
};
if (direction[pressed]) {
let target = event.target;
if (target.index !== undefined) {
if (tabs[target.index + direction[pressed]]) {
tabs[target.index + direction[pressed]].focus();
}
else if (pressed === keys.left || pressed === keys.up) {
focusLastTab();
}
else if (pressed === keys.right || pressed == keys.down) {
focusFirstTab();
};
};
};
};
// Activates any given tab panel
function activateTab (tab, setFocus) {
setFocus = setFocus || true;
// Deactivate all other tabs
deactivateTabs();
// Remove tabindex attribute
tab.removeAttribute('tabindex');
// Set the tab as selected
tab.setAttribute('aria-selected', 'true');
let controls = tab.getAttribute('aria-controls');
// Remove hidden attribute from tab panel to make it visible
document.getElementById(controls).removeAttribute('hidden');
// Set focus when required
if (setFocus) {
tab.focus();
};
};
// Deactivate all tabs and tab panels
function deactivateTabs () {
for (t = 0; t < tabs.length; t++) {
tabs[t].setAttribute('tabindex', '-1');
tabs[t].setAttribute('aria-selected', 'false');
tabs[t].removeEventListener('focus', focusEventHandler);
};
for (p = 0; p < panels.length; p++) {
panels[p].setAttribute('hidden', 'hidden');
};
};
// Focus first tab
function focusFirstTab () {
tabs[0].focus();
};
// Focus last tab
function focusLastTab () {
tabs[tabs.length - 1].focus();
};
// Detect if a tab is deletable
function determineDeletable (event) {
target = event.target;
if (target.getAttribute('data-deletable') !== null) {
// Delete target tab
deleteTab(event, target);
// Update arrays related to tabs widget
generateArrays();
// Activate the closest tab to the one that was just deleted
if (target.index - 1 < 0) {
activateTab(tabs[0]);
}
else {
activateTab(tabs[target.index - 1]);
};
};
};
// Deletes a tab and its panel
function deleteTab (event) {
let target = event.target;
let panel = document.getElementById(target.getAttribute('aria-controls'));
target.parentElement.removeChild(target);
panel.parentElement.removeChild(panel);
};
// Determine whether there should be a delay
// when user navigates with the arrow keys
function determineDelay () {
let hasDelay = tablist.hasAttribute('data-delay');
let delay = 0;
if (hasDelay) {
let delayValue = tablist.getAttribute('data-delay');
if (delayValue) {
delay = delayValue;
}
else {
// If no value is specified, default to 300ms
delay = 300;
};
};
return delay;
};
//
function focusEventHandler (event) {
let target = event.target;
setTimeout(checkTabFocus, delay, target);
};
// Only activate tab on focus if it still has focus after the delay
function checkTabFocus (target) {
focused = document.activeElement;
if (target === focused) {
activateTab(target, false);
};
};
}());
});