
This tutorial walks through building working tabs with HTML, CSS and JavaScript. The idea is simple: add the CSS and JavaScript files to the page, apply a few classes to nested divs, and it works. We will also try to keep the markup semantically correct by putting ARIA attributes on the relevant elements. Do not take that part too seriously, though, as I am not certain I got it exactly right. Either way, giving your HTML some meaning is always worth the effort.
See the Pen
Onglets HTLM/CSS/JS by Damien Flandrin (@dam62500)
on CodePen.0
The HTML structure of the tabs
Here is what the markup can look like. A div with the class tabs, then two divs inside it, one with the class tab-registers and one with the class tab-bodies. The first button inside tab-register is tied to the first element inside tab-bodies. One of the buttons can be given the class active-tab so that it becomes the active tab.
HTML tabs
<div class="tabs">
<div class="tab-registers">
<button class="active-tab">Tab 1</button>
<button>Tab 2</button>
<button>Tab 2</button>
</div>
<div class="tab-bodies">
<div style="display:block;">
Contenu Tab 1
</div>
<div style="display:none;">
Contenu Tab 2
</div>
<div style="display:none;">
Contenu Tab 3
</div>
</div>
</div>The JavaScript behind the tabs
We start by grabbing an HTML collection of every element carrying the class tabs, and we loop over it after turning it into a list with Array.from(list_like). The loop callback receives the element itself and its index. We need that index to identify each group of tabs uniquely.
JavaScript tabs
Array.from(document.querySelectorAll('.tabs')).forEach((tab_container, TabID) => {
...
})Next we store the elements we care about in variables, namely tabs-registers and tab-bodies.
JavaScript tabs
const registers = tab_container.querySelector('.tab-registers');
const bodies = tab_container.querySelector('.tab-bodies');Moving on, we fetch the active register. It may be undefined, in which case we fall back to the first button. Either way, we add the class active-tab to the element.
JavaScript tabs
let activeRegister = registers.querySelector('.active-tab');
activeRegister = activeRegister ? activeRegister : registers.children[0]
activeRegister.classList.add('active-tab')After that we call changeBody(), which does exactly what its name says: it hides every body except the one matching the current register, that is, the current tab. We call the same function every time the tab changes.
We then loop over every element sitting directly inside the register container, which is to say the buttons, the tabs themselves. We set the aria-controls attribute on each one, which declares the element it controls, and we set the matching id on the corresponding body. That id is built from the id of the tab group and the index of the tab button, so it is unique.
Then we attach an event listener to the button: we remove the active tab class from the current register, replace the content of the variable with the new current register, and give it the active tab class in turn. Finally we call changeBody().
JavaScript tabs
Array.from(registers.children).forEach((el, i) => {
el.setAttribute('aria-controls', `${TabID}_${i}`)
bodies.children[i]?.setAttribute('id', `${TabID}_${i}`)
el.addEventListener('click', (ev) => {
let activeRegister = registers.querySelector('.active-tab');
activeRegister.classList.remove('active-tab')
activeRegister = el;
activeRegister.classList.add('active-tab')
changeBody(registers, bodies, activeRegister)
})
})Now to changeBody() itself. Inside it, we loop over the child registers again and take the index of each iteration. If a body exists at that index, we set its display property to block or none depending on the current register. That check is there because there can be more registers than bodies. We also set the aria-expanded attribute on the button, which tells the user whether the panel, or the button leading to a panel, is expanded or not.
JavaScript tabs
function changeBody() {
Array.from(registers.children).forEach((el, i) => {
if (bodies.children[i]) {
bodies.children[i].style.display = el == activeRegister ? 'block' : 'none'
}
el.setAttribute('aria-expanded', el == activeRegister ? 'true' : 'false')
})
}The CSS for the tabs
CSS tabs
.tabs {
font-family: 'Lucida Sans', sans-serif;
font-size: 20px;
}
.tabs .tab-registers {
display: flex;
background-color: RGB(255, 255, 255);
}
.tabs button {
padding: 0.5em;
background-color: RGB(255, 255, 255);
border: none;
font: inherit;
}
.tabs .tab-registers button:hover {
cursor: pointer;
}
.tabs .tab-bodies {
padding: 0.5em;
background-color: RGB(235, 235, 235);
flex-grow: 1;
overflow-y: auto;
}
.tabs button.active-tab {
background-color: rgb(235, 235, 235);
}

