1 Pure CSS7 with JSPublished 2026-10-04Free · no sign-up
Eight tab interfaces that follow the ARIA tabs pattern, with arrow-key navigation and properly linked panels: underline tabs, pill tabs with a sliding highlight, vertical settings tabs, a no-JavaScript version, browser-style boxed tabs, tabs with count badges, scrollable mobile tabs and cross-fading panels.
Tabs that follow the ARIA tabs pattern: a tablist, tab buttons with aria-selected and aria-controls, and tabpanels. Arrow keys move between tabs, Home and End jump to the ends, and only the active tab is in the Tab order, exactly as screen reader users expect.
const tabs = [...document.querySelectorAll('[role=tab]')];
function select(t) {
tabs.forEach(x => { const on = x === t; x.setAttribute('aria-selected', on); x.tabIndex = on ? 0 : -1; document.getElementById(x.getAttribute('aria-controls')).hidden = !on; });
t.focus();
}
tabs.forEach((t, i) => {
t.addEventListener('click', () => select(t));
t.addEventListener('keydown', e => {
const k = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 }[e.key];
if (k === undefined) return;
e.preventDefault();
select(tabs[(k + tabs.length) % tabs.length]);
});
});
02. Pill Tabs with Sliding Indicator
TabsAnimation
Rounded pill tabs where a highlight glides behind the active tab. The indicator's position and width are read from the selected button and written to custom properties, so it adapts to any label length, and keyboard support matches the ARIA pattern.
A vertical tab list on the left with panels on the right, the layout used for account settings. aria-orientation="vertical" tells assistive technology that Up and Down arrows move between tabs, and the layout stacks on narrow screens.
const vt = [...document.querySelectorAll('.vt [role=tab]')];
function go(t) { vt.forEach(x => { const on = x === t; x.setAttribute('aria-selected', on); x.tabIndex = on ? 0 : -1; document.getElementById(x.getAttribute('aria-controls')).hidden = !on; }); t.focus(); }
vt.forEach((t, i) => {
t.addEventListener('click', () => go(t));
t.addEventListener('keydown', e => {
const d = e.key === 'ArrowDown' ? 1 : e.key === 'ArrowUp' ? -1 : 0;
if (d) { e.preventDefault(); go(vt[(i + d + vt.length) % vt.length]); }
});
});
04. CSS-Only Tabs with Radios
Pure CSSTabs
Tabs that work without JavaScript, using hidden radio buttons and the :checked selector to show each panel. They are a reasonable fallback for static pages; note that they behave like a radio group rather than ARIA tabs for screen readers.
View code
<div class="rt">
<input type="radio" name="rt" id="r1" checked><label for="r1">HTML</label>
<input type="radio" name="rt" id="r2"><label for="r2">CSS</label>
<input type="radio" name="rt" id="r3"><label for="r3">JavaScript</label>
<div class="pn">Structure: elements, attributes and meaning.</div>
<div class="pn">Presentation: layout, colour and type.</div>
<div class="pn">Behaviour: events, state and data.</div>
</div>
Folder-style tabs where the selected tab joins the panel below, like browser or file tabs. The selected tab shares the panel's background and covers the panel's top border with a negative margin, so the two read as one piece.
Tabs with an icon, a label and a count badge, such as an inbox with All, Unread and Archived. The badge numbers are part of the tab's accessible name, so a screen reader announces Unread, 4, and the active tab gets a coloured underline.
const ib = [...document.querySelectorAll('.it button')];
ib.forEach((t, i) => {
t.addEventListener('click', () => pick(t));
t.addEventListener('keydown', e => { const d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0; if (d) pick(ib[(i + d + ib.length) % ib.length]); });
});
function pick(t) { ib.forEach(x => { x.setAttribute('aria-selected', x === t); x.tabIndex = x === t ? 0 : -1; }); t.focus(); }
07. Scrollable Tabs for Mobile
TabsResponsive
A long row of category tabs that scrolls sideways on small screens instead of wrapping. Scroll snapping keeps tabs aligned, faded edges hint that there is more, and the selected tab scrolls itself into view when chosen with the keyboard.
const sb = [...document.querySelectorAll('.sct button')];
sb.forEach((t, i) => {
t.addEventListener('click', () => pick(t));
t.addEventListener('keydown', e => { const d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0; if (d) pick(sb[(i + d + sb.length) % sb.length]); });
});
function pick(t) { sb.forEach(x => { x.setAttribute('aria-selected', x === t); x.tabIndex = x === t ? 0 : -1; }); t.focus(); t.scrollIntoView({ inline: 'nearest', block: 'nearest' }); }
08. Segmented Tabs with Fading Panels
TabsTransition
A segmented control drives panels that cross-fade instead of jumping. Panels are stacked in one grid cell so the container keeps the height of the tallest panel, which avoids layout shift when switching, and hidden panels are made inert.
const st = [...document.querySelectorAll('.sgl button')];
st.forEach((t, i) => {
t.addEventListener('click', () => pick(t));
t.addEventListener('keydown', e => { if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') pick(st[1 - i]); });
});
function pick(t) {
st.forEach(x => {
const on = x === t, p = document.getElementById(x.getAttribute('aria-controls'));
x.setAttribute('aria-selected', on); x.tabIndex = on ? 0 : -1;
p.classList.toggle('on', on); p.inert = !on;
});
t.focus();
}
About these CSS Tabs
The ARIA tabs pattern
A tab interface has a role="tablist" container, buttons with role="tab", aria-selected and aria-controls, and panels with role="tabpanel". Only the selected tab is in the Tab order; the arrow keys move between tabs, and Home and End jump to the first and last.
Animating the indicator
The pill demo measures the selected button and writes its left offset and width into custom properties. One absolutely positioned element then slides and stretches between tabs with a CSS transition, which works for labels of any length.
Avoiding layout shift
When panels have different heights, switching tabs can make the page jump. Stacking panels in one grid cell, as the fading demo does, keeps the container at the height of the tallest panel.
When CSS-only is enough
Tabs made from radio buttons work without JavaScript and are fine for static content, but they behave like a radio group rather than tabs for assistive technology. Use the ARIA version for application interfaces.
Frequently asked questions
Should tabs change the URL?
Not usually. If each tab is a separate page of content worth sharing, use real links instead of tabs.
How many tabs are too many?
Beyond six or seven, consider scrollable tabs or a select menu on small screens.
Do these work with a keyboard?
Yes. Every JavaScript demo supports the arrow keys, and the CSS-only demo uses native radio behaviour.