How to Create Tabs in HTML and CSS
This recipe builds a tabbed interface where clicking a tab swaps the visible panel, using a small vanilla JavaScript toggle and ARIA tab roles.
The anatomy of a tab widget
A tab widget has three moving parts: a tab list of buttons, a set of panels (one per tab), and a script that, on click, shows the matching panel while hiding the rest and marks the clicked tab as the active one. Only one panel is ever visible at a time - that is what separates tabs from an accordion, where several panels can be open together.
Building it
Each tab carries a data-target attribute naming the id of the panel it controls. The click handler loops over every tab and panel, removes the active class and aria-selected flag from all of them, then adds it back only to the one that was clicked and the panel it points to. Deactivating everything first, then activating just the one clicked, is simpler and less bug-prone than trying to toggle only two elements by hand.
Example
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: sans-serif; margin: 40px; }
.tabs { display: flex; gap: 4px; border-bottom: 2px solid #e5e7eb; }
.tab {
padding: 10px 20px;
background: none;
border: none;
cursor: pointer;
font-size: 1rem;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
.tab.active {
border-bottom-color: #2563eb;
color: #2563eb;
font-weight: bold;
}
.panel { display: none; padding: 20px 4px; }
.panel.active { display: block; }
</style>
</head>
<body>
<div class='tabs' role='tablist'>
<button class='tab active' role='tab' aria-selected='true' aria-controls='panel-1' data-target='panel-1'>Overview</button>
<button class='tab' role='tab' aria-selected='false' aria-controls='panel-2' data-target='panel-2'>Pricing</button>
<button class='tab' role='tab' aria-selected='false' aria-controls='panel-3' data-target='panel-3'>FAQ</button>
</div>
<div class='panel active' id='panel-1' role='tabpanel'>Overview content goes here.</div>
<div class='panel' id='panel-2' role='tabpanel'>Pricing details go here.</div>
<div class='panel' id='panel-3' role='tabpanel'>Frequently asked questions go here.</div>
<script>
const tabs = document.querySelectorAll('.tab');
const panels = document.querySelectorAll('.panel');
tabs.forEach((tab) => {
tab.addEventListener('click', () => {
tabs.forEach((t) => { t.classList.remove('active'); t.setAttribute('aria-selected', 'false'); });
panels.forEach((p) => p.classList.remove('active'));
tab.classList.add('active');
tab.setAttribute('aria-selected', 'true');
document.getElementById(tab.dataset.target).classList.add('active');
});
});
</script>
</body>
</html>Keyboard and screen-reader support
role='tablist' on the wrapper, role='tab' on each button, and role='tabpanel' on each panel tell assistive technology what the widget is. aria-selected reflects which tab is active, and aria-controls links a tab to the id of the panel it opens. A further enhancement is a roving tabindex - only the active tab has tabindex='0' and is reachable with Tab, while ArrowLeft and ArrowRight move focus between tabs directly, which matches how native tab controls behave.
Example
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<div class='tabs' role='tablist' id='tabList'>
<button role='tab' aria-selected='true' tabindex='0'>Overview</button>
<button role='tab' aria-selected='false' tabindex='-1'>Pricing</button>
<button role='tab' aria-selected='false' tabindex='-1'>FAQ</button>
</div>
<script>
const tabList = document.getElementById('tabList');
const tabs = tabList.querySelectorAll('[role=tab]');
tabList.addEventListener('keydown', (e) => {
const list = Array.from(tabs);
const i = list.indexOf(document.activeElement);
if (e.key === 'ArrowRight') list[(i + 1) % list.length].focus();
if (e.key === 'ArrowLeft') list[(i - 1 + list.length) % list.length].focus();
});
</script>
</body>
</html>Tabs versus an accordion
Tabs and accordions solve a similar problem - showing one section of content at a time - but fit different content. Tabs suit a handful of equally important, unrelated views that never need to be seen side by side, like Overview, Pricing, and FAQ. An accordion suits a longer list of items, like FAQ questions, where a user might reasonably want more than one open at once.
Frequently Asked Questions
- How do I create tabs in HTML and CSS?
- Render one button per tab and one panel per tab. Hide every panel except the active one, and swap an
activeclass on click. CSS controls which panel is visible; JavaScript only moves the class. - How do I keep the selected tab after a page reload?
- Store the active tab in the URL hash or in
localStoragewhen it changes, then read it back on load and apply the active class. The hash approach also makes individual tabs linkable and shareable.