SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/tabs

8 CSS Tabs

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.

01. Accessible Tabs

TabsARIA

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.

View code
<div class="tabs">
  <div role="tablist" aria-label="Product information">
    <button role="tab" id="t1" aria-selected="true" aria-controls="p1">Details</button>
    <button role="tab" id="t2" aria-selected="false" aria-controls="p2" tabindex="-1">Care</button>
    <button role="tab" id="t3" aria-selected="false" aria-controls="p3" tabindex="-1">Delivery</button>
  </div>
  <div role="tabpanel" id="p1" aria-labelledby="t1" tabindex="0">Hand-thrown stoneware, 350 ml, food-safe glaze.</div>
  <div role="tabpanel" id="p2" aria-labelledby="t2" tabindex="0" hidden>Dishwasher safe on a gentle cycle. Avoid sudden temperature changes.</div>
  <div role="tabpanel" id="p3" aria-labelledby="t3" tabindex="0" hidden>Ships in 2 working days. Free over $60.</div>
</div>
.tabs { width: min(440px, 92vw); font-family: Inter, system-ui, sans-serif; }
[role=tablist] { display: flex; gap: 4px; border-bottom: 1px solid #e4e7ec; }
[role=tab] { padding: 10px 16px; border: 0; border-bottom: 2px solid transparent; margin-bottom: -1px; background: none; color: #475467; font: 600 14px Inter, sans-serif; cursor: pointer; }
[role=tab][aria-selected=true] { border-bottom-color: #2563eb; color: #101828; }
[role=tab]:focus-visible { outline: 2px solid #2563eb; outline-offset: -2px; border-radius: 6px 6px 0 0; }
[role=tabpanel] { padding: 18px 4px; color: #344054; font-size: 15px; line-height: 1.55; }
[role=tabpanel]:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; border-radius: 6px; }
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.

View code
<div class="pt">
  <div role="tablist" aria-label="Report period" class="pl">
    <span class="ind" aria-hidden="true"></span>
    <button role="tab" aria-selected="true" aria-controls="pp">Today</button>
    <button role="tab" aria-selected="false" aria-controls="pp" tabindex="-1">This week</button>
    <button role="tab" aria-selected="false" aria-controls="pp" tabindex="-1">This month</button>
  </div>
  <p role="tabpanel" id="pp" class="pv">Revenue today: <b>$1,284</b></p>
</div>
.pt { font-family: Inter, system-ui, sans-serif; }
.pl { position: relative; display: inline-flex; padding: 4px; border-radius: 999px; background: #1a1f2c; }
.ind { position: absolute; top: 4px; bottom: 4px; left: var(--x, 4px); width: var(--w, 70px); border-radius: 999px; background: #fff; transition: left .25s, width .25s; }
.pl button { position: relative; z-index: 1; padding: 9px 16px; border: 0; border-radius: 999px; background: none; color: #aab3c7; font: 600 14px Inter, sans-serif; cursor: pointer; transition: color .2s; }
.pl button[aria-selected=true] { color: #0e1118; }
.pl button:focus-visible { outline: 2px solid #9db8ff; outline-offset: 2px; }
.pv { margin: 18px 0 0; color: #c9d1e2; font-size: 15px; }
.pv b { color: #fff; }
@media (prefers-reduced-motion: reduce) { .ind { transition: none; } }
const list = document.querySelector('.pl'), btns = [...list.querySelectorAll('[role=tab]')];
const data = { 'Today': '$1,284', 'This week': '$8,930', 'This month': '$36,410' };
function pick(b) {
  btns.forEach(x => { x.setAttribute('aria-selected', x === b); x.tabIndex = x === b ? 0 : -1; });
  list.style.setProperty('--x', b.offsetLeft + 'px');
  list.style.setProperty('--w', b.offsetWidth + 'px');
  document.getElementById('pp').innerHTML = 'Revenue ' + b.textContent.toLowerCase() + ': <b>' + data[b.textContent] + '</b>';
}
btns.forEach((b, i) => {
  b.addEventListener('click', () => { pick(b); b.focus(); });
  b.addEventListener('keydown', e => {
    const d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
    if (!d) return;
    const n = btns[(i + d + btns.length) % btns.length]; pick(n); n.focus();
  });
});
pick(btns[0]);

03. Vertical Settings Tabs

TabsSettings

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.

View code
<div class="vt">
  <div role="tablist" aria-label="Settings" aria-orientation="vertical">
    <button role="tab" aria-selected="true" aria-controls="v1">Profile</button>
    <button role="tab" aria-selected="false" aria-controls="v2" tabindex="-1">Security</button>
    <button role="tab" aria-selected="false" aria-controls="v3" tabindex="-1">Billing</button>
  </div>
  <section role="tabpanel" id="v1"><h3>Profile</h3><p>Your name, photo and shop description.</p></section>
  <section role="tabpanel" id="v2" hidden><h3>Security</h3><p>Password, two-step sign-in and active sessions.</p></section>
  <section role="tabpanel" id="v3" hidden><h3>Billing</h3><p>Plan, payment method and invoices.</p></section>
</div>
.vt { box-sizing: border-box; max-width: 100%; display: grid; grid-template-columns: 150px 1fr; gap: 18px; width: min(520px, 94vw); padding: 14px; border-radius: 16px; background: #fff; box-shadow: 0 10px 30px -14px rgba(16,24,40,.3); font-family: Inter, system-ui, sans-serif; }
.vt [role=tablist] { display: grid; align-content: start; gap: 2px; }
.vt [role=tab] { padding: 10px 12px; border: 0; border-radius: 8px; background: none; color: #475467; font: 600 14px Inter, sans-serif; text-align: left; cursor: pointer; }
.vt [role=tab][aria-selected=true] { background: #eef4ff; color: #1d4ed8; }
.vt [role=tab]:focus-visible { outline: 2px solid #2563eb; }
.vt h3 { margin: 4px 0 6px; color: #101828; font-size: 17px; }
.vt p { margin: 0; color: #475467; font-size: 14.5px; }
@media (max-width: 520px) { .vt { grid-template-columns: 1fr; } .vt [role=tablist] { display: flex; } }
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>
.rt { display: flex; flex-wrap: wrap; width: min(420px, 92vw); font-family: Inter, system-ui, sans-serif; }
.rt input { position: absolute; opacity: 0; }
.rt label { padding: 10px 16px; border-radius: 10px 10px 0 0; color: #9aa4b8; font-weight: 600; font-size: 14px; cursor: pointer; }
.rt input:checked + label { background: #1a1f2c; color: #fff; }
.rt input:focus-visible + label { outline: 2px solid #34d399; outline-offset: -2px; }
.pn { display: none; order: 1; width: 100%; padding: 18px; border-radius: 0 12px 12px 12px; background: #1a1f2c; color: #c9d1e2; font-size: 15px; }
#r1:checked ~ .pn:nth-of-type(1), #r2:checked ~ .pn:nth-of-type(2), #r3:checked ~ .pn:nth-of-type(3) { display: block; }

05. Boxed Browser Tabs

TabsClassic

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.

View code
<div class="bx">
  <div role="tablist" aria-label="Files">
    <button role="tab" aria-selected="true" aria-controls="bxp">index.html</button>
    <button role="tab" aria-selected="false" aria-controls="bxp" tabindex="-1">style.css</button>
    <button role="tab" aria-selected="false" aria-controls="bxp" tabindex="-1">app.js</button>
  </div>
  <pre role="tabpanel" id="bxp" tabindex="0">&lt;h1&gt;Hello&lt;/h1&gt;</pre>
</div>
.bx { width: min(440px, 92vw); font-family: Inter, system-ui, sans-serif; }
.bx [role=tablist] { display: flex; gap: 4px; padding-left: 8px; }
.bx [role=tab] { position: relative; padding: 9px 16px; border: 1px solid #d0d5dd; border-bottom: 0; border-radius: 10px 10px 0 0; background: #eef0f4; color: #475467; font: 600 13px "JetBrains Mono", monospace; cursor: pointer; }
.bx [role=tab][aria-selected=true] { z-index: 1; margin-bottom: -1px; background: #fff; color: #101828; }
.bx [role=tab]:focus-visible { outline: 2px solid #2563eb; outline-offset: -3px; }
.bx pre { margin: 0; padding: 18px; border: 1px solid #d0d5dd; border-radius: 12px; background: #fff; color: #101828; font: 14px "JetBrains Mono", monospace; }
.bx pre:focus-visible { outline: 2px solid #2563eb; }
const files = { 'index.html': '<h1>Hello</h1>', 'style.css': 'h1 { color: tomato; }', 'app.js': "console.log('ready');" };
const bt = [...document.querySelectorAll('.bx [role=tab]')];
bt.forEach((t, i) => {
  t.addEventListener('click', () => sel(t));
  t.addEventListener('keydown', e => { const d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0; if (d) sel(bt[(i + d + bt.length) % bt.length]); });
});
function sel(t) { bt.forEach(x => { x.setAttribute('aria-selected', x === t); x.tabIndex = x === t ? 0 : -1; }); document.getElementById('bxp').textContent = files[t.textContent]; t.focus(); }

06. Icon Tabs with Badges

TabsBadge

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.

View code
<div role="tablist" aria-label="Inbox" class="it">
  <button role="tab" aria-selected="false" tabindex="-1"><span aria-hidden="true">&#9993;</span> All <b>28</b></button>
  <button role="tab" aria-selected="true"><span aria-hidden="true">&#9679;</span> Unread <b>4</b></button>
  <button role="tab" aria-selected="false" tabindex="-1"><span aria-hidden="true">&#9636;</span> Archived <b>112</b></button>
</div>
.it { box-sizing: border-box; max-width: 100%; display: flex; flex-wrap: wrap; gap: 6px; border-bottom: 1px solid #262d3e; font-family: Inter, system-ui, sans-serif; }
.it button { display: inline-flex; align-items: center; gap: 7px; padding: 12px 14px; border: 0; border-bottom: 2px solid transparent; margin-bottom: -1px; background: none; color: #9aa4b8; font: 600 14px Inter, sans-serif; cursor: pointer; }
.it b { padding: 1px 7px; border-radius: 999px; background: #262d3e; color: #c9d1e2; font-size: 11.5px; }
.it button[aria-selected=true] { border-bottom-color: #22d3ee; color: #fff; }
.it button[aria-selected=true] b { background: #22d3ee; color: #062a33; }
.it button:focus-visible { outline: 2px solid #67e8f9; outline-offset: -2px; }
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.

View code
<div class="sc">
  <div role="tablist" aria-label="Categories" class="sct">
    <button role="tab" aria-selected="true">All</button>
    <button role="tab" aria-selected="false" tabindex="-1">Mugs</button>
    <button role="tab" aria-selected="false" tabindex="-1">Bowls</button>
    <button role="tab" aria-selected="false" tabindex="-1">Plates</button>
    <button role="tab" aria-selected="false" tabindex="-1">Vases</button>
    <button role="tab" aria-selected="false" tabindex="-1">Planters</button>
    <button role="tab" aria-selected="false" tabindex="-1">Gift sets</button>
  </div>
</div>
.sc { width: min(320px, 88vw); font-family: Inter, system-ui, sans-serif; mask-image: linear-gradient(90deg, transparent, #000 16px, #000 calc(100% - 24px), transparent); }
.sct { display: flex; gap: 8px; padding: 4px 16px; overflow-x: auto; scroll-snap-type: x mandatory; scrollbar-width: none; }
.sct::-webkit-scrollbar { display: none; }
.sct button { flex: none; scroll-snap-align: start; padding: 8px 14px; border: 1.5px solid #d0d5dd; border-radius: 999px; background: #fff; color: #344054; font: 600 13.5px Inter, sans-serif; cursor: pointer; }
.sct button[aria-selected=true] { border-color: #101828; background: #101828; color: #fff; }
.sct button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
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.

View code
<div class="sg">
  <div role="tablist" aria-label="Plan" class="sgl">
    <button role="tab" aria-selected="true" aria-controls="sg1">Personal</button>
    <button role="tab" aria-selected="false" aria-controls="sg2" tabindex="-1">Team</button>
  </div>
  <div class="sgp">
    <div role="tabpanel" id="sg1" class="on"><b>$8 / month</b><p>One user, unlimited projects.</p></div>
    <div role="tabpanel" id="sg2" inert><b>$24 / month</b><p>Up to 10 users, shared folders and roles.</p></div>
  </div>
</div>
.sg { width: min(360px, 90vw); font-family: Inter, system-ui, sans-serif; }
.sgl { display: grid; grid-template-columns: 1fr 1fr; padding: 4px; border-radius: 12px; background: #1a1f2c; }
.sgl button { padding: 10px; border: 0; border-radius: 9px; background: none; color: #9aa4b8; font: 600 14px Inter, sans-serif; cursor: pointer; }
.sgl button[aria-selected=true] { background: #a78bfa; color: #160f2e; }
.sgl button:focus-visible { outline: 2px solid #c4b5fd; outline-offset: 2px; }
.sgp { display: grid; margin-top: 14px; }
.sgp > div { grid-area: 1 / 1; padding: 18px; border: 1px solid #262d3e; border-radius: 14px; color: #aab3c7; opacity: 0; transition: opacity .25s; }
.sgp > div.on { opacity: 1; }
.sgp b { color: #fff; font-size: 22px; }
.sgp p { margin: 6px 0 0; font-size: 14px; }
@media (prefers-reduced-motion: reduce) { .sgp > div { transition: none; } }
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.