Open demo

CSS code

HTML
<div class="atabs">
  <div role="tablist" aria-label="Shipping info">
    <button type="button" role="tab" id="at-tab-0" aria-selected="true" aria-controls="at-panel-0">Shipping</button>
    <button type="button" role="tab" id="at-tab-1" aria-selected="false" aria-controls="at-panel-1" tabindex="-1">Returns</button>
    <button type="button" role="tab" id="at-tab-2" aria-selected="false" aria-controls="at-panel-2" tabindex="-1">Warranty</button>
  </div>
  <button type="button" class="acc-head" id="at-head-0" aria-expanded="true" aria-controls="at-panel-0">Shipping</button>
  <div role="tabpanel" id="at-panel-0" class="panel" aria-labelledby="at-tab-0" tabindex="0">Orders ship within 24 hours from our EU warehouse. Tracked delivery takes 2–4 business days; express upgrades are available at checkout.</div>
  <button type="button" class="acc-head" id="at-head-1" aria-expanded="false" aria-controls="at-panel-1">Returns</button>
  <div role="tabpanel" id="at-panel-1" class="panel" aria-labelledby="at-tab-1" tabindex="0" hidden>Thirty days, no questions. Start a return from your account page and drop the parcel at any pickup point — the label is prepaid.</div>
  <button type="button" class="acc-head" id="at-head-2" aria-expanded="false" aria-controls="at-panel-2">Warranty</button>
  <div role="tabpanel" id="at-panel-2" class="panel" aria-labelledby="at-tab-2" tabindex="0" hidden>Two years against manufacturing defects. Register your product to extend it to three at no cost.</div>
</div>
CSS
/* Responsive tabs: a tab row on wide screens; under 560px the same panels are driven by accordion headers instead. */
.atabs{--at-accent:#b45309;--at-line:#e7dcc9;--at-muted:#76674d;--at-text:#292219;
  box-sizing:border-box;width:100%;max-width:560px;color:var(--at-text)}
.atabs [role=tablist]{display:flex;gap:6px;margin-bottom:14px}
.atabs [role=tab]{flex:1;margin:0;border:1.5px solid var(--at-line);background:#fff;padding:11px;border-radius:11px;font:inherit;font-size:.87rem;font-weight:700;
  color:var(--at-muted);cursor:pointer;transition:background-color .15s,border-color .15s,color .15s,box-shadow .15s}
.atabs [role=tab][aria-selected=true]{background:var(--at-accent);border-color:var(--at-accent);color:#fff;box-shadow:0 8px 18px -8px rgba(180,83,9,.5)}
.atabs .acc-head{display:none}
.atabs .panel{background:#fff;border:1.5px solid var(--at-line);border-radius:13px;padding:20px 22px;font-size:.89rem;line-height:1.7;color:#6d5f49}
.atabs [role=tab]:focus-visible,.atabs .acc-head:focus-visible{outline:2px solid var(--at-accent);outline-offset:2px}
.atabs .panel:focus-visible{outline:2px solid var(--at-accent);outline-offset:2px}
.atabs [hidden]{display:none}
@media (max-width:560px){
  .atabs [role=tablist]{display:none}
  .atabs .acc-head{display:block;width:100%;margin:0 0 8px;text-align:left;border:1.5px solid var(--at-line);background:#fff;padding:13px 16px;border-radius:11px;
    font:inherit;font-size:.9rem;font-weight:800;color:var(--at-text);cursor:pointer}
  .atabs .acc-head[aria-expanded=true]{background:var(--at-accent);border-color:var(--at-accent);color:#fff;border-radius:11px 11px 0 0;margin-bottom:0}
  .atabs .panel{border-radius:0 0 11px 11px;margin-bottom:8px;border-top:0}
}
@media (prefers-reduced-motion:reduce){.atabs [role=tab]{transition:none}}
JavaScript
/* Tabs on wide screens (arrow keys, Home/End, roving tabindex); accordion headers under 560px.
   Both drive the same panels, and each panel's role follows whichever control is on screen. */
document.querySelectorAll('.atabs').forEach(root => {
  const list = root.querySelector('[role=tablist]');
  const tabs = [...list.querySelectorAll('[role=tab]')], heads = [...root.querySelectorAll('.acc-head')];
  const panels = tabs.map(t => document.getElementById(t.getAttribute('aria-controls')));
  const select = (i, focus) => {
    tabs.forEach((t, k) => {
      t.setAttribute('aria-selected', k === i);
      t.tabIndex = k === i ? 0 : -1;
      heads[k].setAttribute('aria-expanded', k === i);
      panels[k].hidden = k !== i;
    });
    if (focus) tabs[i].focus();
  };
  list.addEventListener('click', e => { const t = e.target.closest('[role=tab]'); if (t) select(tabs.indexOf(t)); });
  heads.forEach((h, k) => h.addEventListener('click', () => select(k)));
  list.addEventListener('keydown', e => {
    const i = tabs.indexOf(e.target), n = tabs.length;
    const j = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1 + n, ArrowUp: i - 1 + n, Home: 0, End: n - 1 }[e.key];
    if (i < 0 || j === undefined) return;
    e.preventDefault();
    select(j % n, true);
  });
  const narrow = matchMedia('(max-width: 560px)');
  const mode = () => panels.forEach((p, k) => {
    p.setAttribute('role', narrow.matches ? 'region' : 'tabpanel');
    p.setAttribute('aria-labelledby', (narrow.matches ? heads[k] : tabs[k]).id);
    if (narrow.matches) p.removeAttribute('tabindex'); else p.tabIndex = 0;
  });
  narrow.addEventListener('change', mode); mode();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div class="atabs">
  <ul class="nav nav-pills nav-justified" role="tablist" aria-label="Shipping info">
    <li class="nav-item" role="presentation"><button class="nav-link active" type="button" id="at-tab-0" data-bs-toggle="pill" data-bs-target="#at-panel-0" role="tab" aria-controls="at-panel-0" aria-selected="true">Shipping</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="at-tab-1" data-bs-toggle="pill" data-bs-target="#at-panel-1" role="tab" aria-controls="at-panel-1" aria-selected="false" tabindex="-1">Returns</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="at-tab-2" data-bs-toggle="pill" data-bs-target="#at-panel-2" role="tab" aria-controls="at-panel-2" aria-selected="false" tabindex="-1">Warranty</button></li>
  </ul>
  <div class="tab-content">
    <button type="button" class="acc-head" id="at-head-0" aria-expanded="true" aria-controls="at-panel-0">Shipping</button>
    <div class="tab-pane active" id="at-panel-0" role="tabpanel" aria-labelledby="at-tab-0" tabindex="0">Orders ship within 24 hours from our EU warehouse. Tracked delivery takes 2–4 business days; express upgrades are available at checkout.</div>
    <button type="button" class="acc-head" id="at-head-1" aria-expanded="false" aria-controls="at-panel-1">Returns</button>
    <div class="tab-pane" id="at-panel-1" role="tabpanel" aria-labelledby="at-tab-1" tabindex="0">Thirty days, no questions. Start a return from your account page and drop the parcel at any pickup point — the label is prepaid.</div>
    <button type="button" class="acc-head" id="at-head-2" aria-expanded="false" aria-controls="at-panel-2">Warranty</button>
    <div class="tab-pane" id="at-panel-2" role="tabpanel" aria-labelledby="at-tab-2" tabindex="0">Two years against manufacturing defects. Register your product to extend it to three at no cost.</div>
  </div>
</div>
CSS
/* Bootstrap .nav-pills.nav-justified with the Tab plugin on wide screens; under 560px accordion headers show the same .tab-pane elements through the plugin. */
.atabs{--at-accent:#b45309;--at-line:#e7dcc9;--at-muted:#76674d;--at-text:#292219;
  width:100%;max-width:560px;color:var(--at-text)}
.atabs .nav-pills{--bs-nav-link-padding-x:11px;--bs-nav-link-padding-y:11px;--bs-nav-link-font-size:.87rem;--bs-nav-link-font-weight:700;
  --bs-nav-link-color:var(--at-muted);--bs-nav-link-hover-color:var(--at-muted);
  --bs-nav-pills-border-radius:11px;--bs-nav-pills-link-active-bg:var(--at-accent);--bs-nav-pills-link-active-color:#fff;
  flex-wrap:nowrap;gap:6px;margin-bottom:14px}
.atabs .nav-link{border:1.5px solid var(--at-line);background:#fff;line-height:normal;transition:background-color .15s,border-color .15s,color .15s,box-shadow .15s}
.atabs .nav-link.active{border-color:var(--at-accent);box-shadow:0 8px 18px -8px rgba(180,83,9,.5)}
.atabs .acc-head{display:none}
.atabs .tab-pane{background:#fff;border:1.5px solid var(--at-line);border-radius:13px;padding:20px 22px;font-size:.89rem;line-height:1.7;color:#6d5f49}
.atabs .nav-link:focus-visible,.atabs .acc-head:focus-visible,.atabs .tab-pane:focus-visible{box-shadow:none;outline:2px solid var(--at-accent);outline-offset:2px}
@media (max-width:560px){
  .atabs .nav-pills{display:none}
  .atabs .acc-head{display:block;width:100%;margin:0 0 8px;text-align:left;border:1.5px solid var(--at-line);background:#fff;padding:13px 16px;border-radius:11px;
    font-size:.9rem;font-weight:800;line-height:normal;color:var(--at-text);cursor:pointer}
  .atabs .acc-head[aria-expanded=true]{background:var(--at-accent);border-color:var(--at-accent);color:#fff;border-radius:11px 11px 0 0;margin-bottom:0}
  .atabs .tab-pane{border-radius:0 0 11px 11px;margin-bottom:8px;border-top:0}
}
@media (prefers-reduced-motion:reduce){.atabs .nav-link{transition:none}}
JavaScript
/* The Tab plugin runs the tabs. Accordion headers call the same plugin, and each pane's role follows whichever control is on screen. */
document.querySelectorAll('.atabs').forEach(root => {
  const tabs = [...root.querySelectorAll('[role=tab]')], heads = [...root.querySelectorAll('.acc-head')];
  const panels = tabs.map(t => document.getElementById(t.getAttribute('aria-controls')));
  heads.forEach((h, k) => h.addEventListener('click', () => bootstrap.Tab.getOrCreateInstance(tabs[k]).show()));
  root.addEventListener('show.bs.tab', e => heads.forEach((h, k) => h.setAttribute('aria-expanded', tabs[k] === e.target)));
  const narrow = matchMedia('(max-width: 560px)');
  const mode = () => panels.forEach((p, k) => {
    p.setAttribute('role', narrow.matches ? 'region' : 'tabpanel');
    p.setAttribute('aria-labelledby', (narrow.matches ? heads[k] : tabs[k]).id);
    if (narrow.matches) p.removeAttribute('tabindex'); else p.tabIndex = 0;
  });
  narrow.addEventListener('change', mode); mode();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="atabs w-full max-w-[560px] text-at-text">
  <div role="tablist" aria-label="Shipping info" class="mb-3.5 flex gap-1.5 narrow:hidden">
    <button type="button" role="tab" id="at-tab-0" aria-selected="true" aria-controls="at-panel-0" class="flex-1 cursor-pointer rounded-[11px] border-[1.5px] border-at-line bg-white p-[11px] text-[.87rem] font-bold text-at-muted transition-[background-color,border-color,color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-at-accent aria-selected:border-at-accent aria-selected:bg-at-accent aria-selected:text-white aria-selected:shadow-[0_8px_18px_-8px_rgba(180,83,9,.5)] motion-reduce:transition-none">Shipping</button>
    <button type="button" role="tab" id="at-tab-1" aria-selected="false" aria-controls="at-panel-1" tabindex="-1" class="flex-1 cursor-pointer rounded-[11px] border-[1.5px] border-at-line bg-white p-[11px] text-[.87rem] font-bold text-at-muted transition-[background-color,border-color,color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-at-accent aria-selected:border-at-accent aria-selected:bg-at-accent aria-selected:text-white aria-selected:shadow-[0_8px_18px_-8px_rgba(180,83,9,.5)] motion-reduce:transition-none">Returns</button>
    <button type="button" role="tab" id="at-tab-2" aria-selected="false" aria-controls="at-panel-2" tabindex="-1" class="flex-1 cursor-pointer rounded-[11px] border-[1.5px] border-at-line bg-white p-[11px] text-[.87rem] font-bold text-at-muted transition-[background-color,border-color,color,box-shadow] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-at-accent aria-selected:border-at-accent aria-selected:bg-at-accent aria-selected:text-white aria-selected:shadow-[0_8px_18px_-8px_rgba(180,83,9,.5)] motion-reduce:transition-none">Warranty</button>
  </div>
  <button type="button" class="acc-head mb-2 hidden w-full cursor-pointer rounded-[11px] border-[1.5px] border-at-line bg-white px-4 py-[13px] text-left text-[.9rem] font-extrabold text-at-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-at-accent narrow:block narrow:aria-expanded:mb-0 narrow:aria-expanded:rounded-b-none narrow:aria-expanded:border-at-accent narrow:aria-expanded:bg-at-accent narrow:aria-expanded:text-white" id="at-head-0" aria-expanded="true" aria-controls="at-panel-0">Shipping</button>
  <div role="tabpanel" id="at-panel-0" aria-labelledby="at-tab-0" tabindex="0" class="rounded-[13px] border-[1.5px] border-at-line bg-white px-[22px] py-5 text-[.89rem] leading-[1.7] text-[#6d5f49] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-at-accent narrow:mb-2 narrow:rounded-t-none narrow:rounded-b-[11px] narrow:border-t-0">Orders ship within 24 hours from our EU warehouse. Tracked delivery takes 2–4 business days; express upgrades are available at checkout.</div>
  <button type="button" class="acc-head mb-2 hidden w-full cursor-pointer rounded-[11px] border-[1.5px] border-at-line bg-white px-4 py-[13px] text-left text-[.9rem] font-extrabold text-at-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-at-accent narrow:block narrow:aria-expanded:mb-0 narrow:aria-expanded:rounded-b-none narrow:aria-expanded:border-at-accent narrow:aria-expanded:bg-at-accent narrow:aria-expanded:text-white" id="at-head-1" aria-expanded="false" aria-controls="at-panel-1">Returns</button>
  <div role="tabpanel" id="at-panel-1" aria-labelledby="at-tab-1" tabindex="0" hidden class="rounded-[13px] border-[1.5px] border-at-line bg-white px-[22px] py-5 text-[.89rem] leading-[1.7] text-[#6d5f49] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-at-accent narrow:mb-2 narrow:rounded-t-none narrow:rounded-b-[11px] narrow:border-t-0">Thirty days, no questions. Start a return from your account page and drop the parcel at any pickup point — the label is prepaid.</div>
  <button type="button" class="acc-head mb-2 hidden w-full cursor-pointer rounded-[11px] border-[1.5px] border-at-line bg-white px-4 py-[13px] text-left text-[.9rem] font-extrabold text-at-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-at-accent narrow:block narrow:aria-expanded:mb-0 narrow:aria-expanded:rounded-b-none narrow:aria-expanded:border-at-accent narrow:aria-expanded:bg-at-accent narrow:aria-expanded:text-white" id="at-head-2" aria-expanded="false" aria-controls="at-panel-2">Warranty</button>
  <div role="tabpanel" id="at-panel-2" aria-labelledby="at-tab-2" tabindex="0" hidden class="rounded-[13px] border-[1.5px] border-at-line bg-white px-[22px] py-5 text-[.89rem] leading-[1.7] text-[#6d5f49] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-at-accent narrow:mb-2 narrow:rounded-t-none narrow:rounded-b-[11px] narrow:border-t-0">Two years against manufacturing defects. Register your product to extend it to three at no cost.</div>
</div>
CSS
@theme {
  --color-at-accent: #b45309;
  --color-at-line: #e7dcc9;
  --color-at-muted: #76674d;
  --color-at-text: #292219;
}
/* max-width:560px, the same breakpoint the script's matchMedia uses */
@custom-variant narrow (@media (max-width: 560px));
JavaScript
/* Tabs on wide screens (arrow keys, Home/End, roving tabindex); accordion headers under 560px.
   Both drive the same panels, and each panel's role follows whichever control is on screen. */
document.querySelectorAll('.atabs').forEach(root => {
  const list = root.querySelector('[role=tablist]');
  const tabs = [...list.querySelectorAll('[role=tab]')], heads = [...root.querySelectorAll('.acc-head')];
  const panels = tabs.map(t => document.getElementById(t.getAttribute('aria-controls')));
  const select = (i, focus) => {
    tabs.forEach((t, k) => {
      t.setAttribute('aria-selected', k === i);
      t.tabIndex = k === i ? 0 : -1;
      heads[k].setAttribute('aria-expanded', k === i);
      panels[k].hidden = k !== i;
    });
    if (focus) tabs[i].focus();
  };
  list.addEventListener('click', e => { const t = e.target.closest('[role=tab]'); if (t) select(tabs.indexOf(t)); });
  heads.forEach((h, k) => h.addEventListener('click', () => select(k)));
  list.addEventListener('keydown', e => {
    const i = tabs.indexOf(e.target), n = tabs.length;
    const j = { ArrowRight: i + 1, ArrowDown: i + 1, ArrowLeft: i - 1 + n, ArrowUp: i - 1 + n, Home: 0, End: n - 1 }[e.key];
    if (i < 0 || j === undefined) return;
    e.preventDefault();
    select(j % n, true);
  });
  const narrow = matchMedia('(max-width: 560px)');
  const mode = () => panels.forEach((p, k) => {
    p.setAttribute('role', narrow.matches ? 'region' : 'tabpanel');
    p.setAttribute('aria-labelledby', (narrow.matches ? heads[k] : tabs[k]).id);
    if (narrow.matches) p.removeAttribute('tabindex'); else p.tabIndex = 0;
  });
  narrow.addEventListener('change', mode); mode();
});

About this set

Three equal-width tabs, Shipping, Returns and Warranty, above a bordered panel on wide screens, which turn into a stacked accordion below 560 pixels. On desktop the selected tab fills with amber; on a phone each section gets its own full-width header, the open one amber with square bottom corners so it joins its panel. Use it for product detail pages, FAQs and policy sections that need tabs on a laptop but would be cramped as a tab row on a phone. Both the tabs and the accordion headers drive the same three panels, so there is no duplicated content and the selection carries over when the window is resized. The script keeps aria-selected on the tabs and aria-expanded on the headers in step, and it swaps each panel’s role between tabpanel and region, labelled by whichever control is on screen, when the breakpoint changes. The Bootstrap version runs the tabs with the Tab plugin and has the accordion headers call bootstrap.Tab, and the Tailwind version uses a custom narrow variant for the breakpoint.

What’s included

  • Tabs on wide screens, accordion under 560px
  • One set of panels shared by both modes
  • Panel roles and labels follow the visible control
  • aria-expanded on accordion headers
  • Muted tab text darkened to meet 4.5:1

Accessibility

On wide screens the tabs follow the WAI-ARIA tabs pattern: only the selected tab is in the Tab order; Left and Right (or Up and Down) move between tabs and select them, and Home and End jump to the first and last tab. Under 560px the tablist is hidden and each header is a button with aria-expanded, and its panel becomes a region labelled by that header. Tabs, headers and panels show a 2px amber outline on keyboard focus, and transitions are removed under prefers-reduced-motion.

Customise it

Set --at-accent, --at-line, --at-muted and --at-text on .atabs in plain CSS and Bootstrap, and change the 560px breakpoint in the media query and in the script's matchMedia together. In Tailwind edit the --color-at-* theme colours and the narrow custom variant.