Accessible Tabs Element
A fully accecible tabs element
HTML
<div class="tabs">
<ul role="tablist" class="tablist">
<li role="presentation">
<a
class="tab"
id="tab1"
href="#section1"
role="tab"
aria-controls="section1"
aria-selected="true"
>Tab One</a
>
</li>
<li role="presentation">
<a
class="tab"
id="tab2"
href="#section2"
role="tab"
aria-controls="section2"
>Tab two</a
>
</li>
<li role="presentation">
<a
class="tab"
id="tab3"
href="#section3"
role="tab"
aria-controls="section3"
>Tab Three</a
>
</li>
</ul>
<div class="tab-section">
<section
class="tabpanel"
id="section1"
role="tabpanel"
aria-labelledby="tab1"
tabindex="0"
>
Section 1
</section>
<section
class="tabpanel"
id="section2"
role="tabpanel"
aria-labelledby="tab2"
tabindex="0"
hidden
>
Section 2
</section>
<section
class="tabpanel"
id="section3"
role="tabpanel"
aria-labelledby="tab3"
tabindex="0"
hidden
>
Section 3
</section>
</div>
</div>
CSS
.tabpanel:not(:target):not(.visible) {
display: none;
}
.tab:focus-visible {
background-color: royalblue;
color: whitesmoke;
outline: 0.2em solid royalblue;
}
.tab[aria-selected='true'] {
background-color: var(--selected-tab, pink);
color: black;
}
JavaScript
const tablist = document.querySelector('.tablist');
const tabs = [...tablist.querySelectorAll('.tab')];
const tabpannels = [...document.querySelectorAll('.tabpanel')];
// Hide all panels except the current tab
const showActivePanel = (element) => {
const selectedId = element.id;
tabpannels.forEach((tabPanel) => {
tabPanel.hidden = 'true';
});
const activePanel = document.querySelector(
`[aria-labelledby="${selectedId}"]`
);
activePanel.removeAttribute('hidden');
};
const setSelectedTab = (element) => {
const selectedId = element.id;
tabpannels[0].classList.remove('visible');
tabs.forEach((tab) => {
const id = tab.getAttribute('id');
if (id === selectedId) {
tab.removeAttribute('tabindex', '0');
tab.setAttribute('aria-selected', 'true');
} else {
tab.setAttribute('tabindex', '-1');
tab.setAttribute('aria-selected', 'false');
}
});
};
const handleOnKeyboardArrows = () => {
const firstTab = tabs[0];
const lastTab = tabs[tabs.length - 1];
tabs.forEach((element) => {
element.addEventListener('keydown', function (e) {
// If arrow up or arrow left
if (
(e.keyCode || e.which) === 38 ||
(e.keyCode || e.which) === 37
) {
// If it is the first tab go to last tab
if (element == firstTab) {
e.preventDefault();
lastTab.focus();
}
// Otherwise focus previus tab
else {
e.preventDefault();
const focusableElement = tabs.indexOf(element) - 1;
tabs[focusableElement].focus();
}
}
// If arrow down or arrow right
else if (
(e.keyCode || e.which) === 40 ||
(e.keyCode || e.which) === 39
) {
// If it is the last tab, go back to the first one
if (element == lastTab) {
e.preventDefault();
firstTab.focus();
}
// Otherwise focus next
else {
e.preventDefault();
const focusableElement = tabs.indexOf(element) + 1;
tabs[focusableElement].focus();
}
}
});
});
};
// If the panel section has focusable elementss inside, make the panel section focusable
const determinePanelTabindex = () => {
tabpannels.forEach((tabpanel) => {
const focusableElements = tabpanel.querySelectorAll(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled]), details:not([disabled]), summary:not(:disabled)'
).length;
focusableElements
? tabpanel.setAttribute('tabindex', '-1')
: tabpanel.setAttribute('tabindex', '0');
});
};
const handleOnClick = () => {
tabs.forEach((tab) => {
tab.addEventListener('click', function () {
setSelectedTab(tab);
showActivePanel(tab);
});
tab.addEventListener('keydown', function (e) {
// on space key (almost like hitting enter)
if ((e.keyCode || e.which) === 32) {
setSelectedTab(tab);
showActivePanel(tab);
tab.click();
}
});
});
};
const activateFirstPanel = () => {
tabs[0].setAttribute('tabindex', '0');
tabs[0].setAttribute('aria-selected', 'true');
tabpannels[0].classList.add('visible');
};
const checkInitialSelectedTab = () => {
const targetedTabPanel = document
.querySelector('.tabpanel:target')
.getAttribute('aria-labelledby');
const selectedTab = document.querySelector(`#${targetedTabPanel}`);
selectedTab.setAttribute('aria-selected', 'true');
selectedTab.removeAttribute('tabindex');
};
const handleInitialState = () => {
tabs.forEach((tab) => {
tab.setAttribute('tabindex', '-1');
tab.setAttribute('aria-selected', 'false');
});
// If no other panel is selected, the default to the first one on load
window.location.href.indexOf('#panel') === -1
? activateFirstPanel()
: checkInitialSelectedTab();
determinePanelTabindex();
};
handleOnClick();
handleOnKeyboardArrows();
window.onload = handleInitialState;
Here is a great Post about accesible tabs.