<toggle-tabs>
Create accessible toggle tab components.
Basic Usage
Wrap a list of anchor links and matching content (with IDs) in the <toggle-tabs>
element.
The list of anchor links should have a [tabs]
attribute on it.
<toggle-tabs>
<ul tabs>
<li><a href="#wizard">Wizard</a></li>
<li><a href="#sorcerer">Sorcerer</a></li>
<li><a href="#druid">Druid</a></li>
</ul>
<div id="wizard">
Wizards gain their magic through study...
</div>
<div id="sorcerer">
Sorcerers get their power from an otherworldly being...
</div>
<div id="druid">
Druids get their power from nature.
</div>
</toggle-tabs>
When the Web Component loads, it will hide the content, add required controls and ARIA attributes, and add event listeners to selectively show or hide the content when a tab is clicked or required keyboard interactions occur.
Styling
The Web Component loads a global <style>
element that includes the minimum required styles to provide appropriate visual affordance.
You can add additional styles as desired, and modify pre-defined styles using CSS variables.
toggle-tabs {
/* The navigation list */
--toggle-tab-list-margin: 0 0 2em;
/* Toggle nav links */
--toggle-tab-link-color: currentColor;
--toggle-tab-link-margin: 0 0 0.25em;
--toggle-tab-link-padding: 0.5em 1em;
/* Toggle nav link :hover */
--toggle-tab-list-hover-bg-color: #f7f7f7;
/* The active toggle nav link */
--toggle-tab-link-active-bg-color: #e5e5e5;
}
The Web Component
customElements.define('toggle-tabs', class extends HTMLElement {
/**
* Instantiate the Web Component
*/
constructor () {
// Get parent class properties
super();
// Define properties
this.tabList = this.querySelector('[tabs]');
// Setup UI
this.setupDOM();
this.loadCSS();
this.clickHandler = this.createClickHandler();
this.keyHandler = this.createKeyHandler();
}
/**
* Add buttons and hide content on page load
*/
setupDOM () {
// Only run if there are tabs
if (!this.tabList) return;
// Get the list items and links
let listItems = this.tabList.querySelectorAll('li');
let links = this.tabList.querySelectorAll('a');
// Add ARIA to list
this.tabList.setAttribute('role', 'tablist');
// Add ARIA to the list items
for (let item of listItems) {
item.setAttribute('role', 'presentation');
}
// Add ARIA to the links and content
let instance = this;
links.forEach(function (link, index) {
// Get the the target element
let tabPane = instance.querySelector(link.hash);
if (!tabPane) return;
// Add [role] and [aria-selected] attributes
link.setAttribute('role', 'tab');
link.setAttribute('aria-selected', index === 0 ? true : false);
// If it's not the active (first) tab, remove focus
if (index > 0) {
link.setAttribute('tabindex', -1);
}
// If there's no ID, add one
if (!link.id) {
link.id = `tab_${tabPane.id}`;
}
// Add ARIA to tab pane
tabPane.setAttribute('role', 'tabpanel');
tabPane.setAttribute('aria-labelledby', link.id);
// If not the active pane, hide it
if (index > 0) {
tabPane.setAttribute('hidden', '');
}
});
}
/**
* Load accordion styles
*/
loadCSS () {
if (document.querySelector('[data-toggle-tabs-css]')) return;
let css = document.createElement('style');
css.innerHTML =
`toggle-tabs [role="tablist"] {
list-style: none;
margin: var(--toggle-tab-list-margin, 0 0 2em);
padding: 0;
}
toggle-tabs [role="tablist"] li {
display: inline-block;
}
toggle-tabs [role="tab"] {
color: var(--toggle-tab-link-color, currentColor);
margin: var(--toggle-tab-link-margin, 0 0 0.25em);
padding: var(--toggle-tab-link-padding, 0.5em 1em);
text-decoration: none;
}
toggle-tabs [role="tab"]:active,
toggle-tabs [role="tab"]:hover {
background-color: var(--toggle-tab-list-hover-bg-color, #f7f7f7);
}
toggle-tabs [role="tab"][aria-selected="true"] {
background-color: var(--toggle-tab-link-active-bg-color, #e5e5e5);
}`;
css.setAttribute('data-toggle-tabs-css', '');
document.head.append(css);
}
/**
* Create the event handler
*/
createClickHandler () {
return (event) => {
// Only run on tab links
if (!event.target.matches('[role="tab"]')) return;
// Prevent the link from updating the URL
event.preventDefault();
// Ignore the currently active tab
if (event.target.matches('[aria-selected="true"]')) return;
// Toggle tab visibility
this.toggle(event.target);
};
}
createKeyHandler () {
return (event) => {
// Only run for left and right arrow keys
if (!['ArrowLeft', 'ArrowRight'].includes(event.code)) return;
// Only run if element in focus is on a tab
let tab = document.activeElement.closest('[role="tab"]');
if (!tab) return;
// Only run if focused tab is in this component
if (!this.tabList.contains(tab)) return;
// Get the currently active tab
let currentTab = this.tabList.querySelector('[role="tab"][aria-selected="true"]');
// Get the parent list item
let listItem = currentTab.closest('li');
// If right arrow, get the next sibling
// Otherwise, get the previous
let nextListItem = event.code === 'ArrowRight' ? listItem.nextElementSibling : listItem.previousElementSibling;
if (!nextListItem) return;
let nextTab = nextListItem.querySelector('a');
// Toggle tab visibility
this.toggle(nextTab);
nextTab.focus();
};
}
/**
* Toggle tab visibility
* @param {Node} tab The tab to show
*/
toggle (tab) {
// Get the target tab pane
let tabPane = this.querySelector(tab.hash);
if (!tabPane) return;
// Get the current tab and content
let currentTab = tab.closest('[role="tablist"]').querySelector('[aria-selected="true"]');
let currentPane = document.querySelector(currentTab.hash);
// Update the selected tab
tab.setAttribute('aria-selected', true);
currentTab.setAttribute('aria-selected', false);
// Update the visible tabPane
tabPane.removeAttribute('hidden');
currentPane.setAttribute('hidden', '');
// Make sure current tab can be focused and other tabs cannot
tab.removeAttribute('tabindex');
currentTab.setAttribute('tabindex', -1);
}
/**
* Start listening to clicks
*/
connectedCallback () {
if (!this.tabList) return;
this.tabList.addEventListener('click', this.clickHandler);
document.addEventListener('keydown', this.keyHandler);
}
/**
* Stop listening to clicks
*/
disconnectedCallback () {
this.tabList.removeEventListener('click', this.clickHandler);
document.removeEventListener('keydown', this.keyHandler);
}
});