Open demo

CSS code

HTML
<div class="stabs">
  <div class="hd"><h2>Release notes</h2><p>Eight releases — the bar scrolls rather than wrapping.</p></div>
  <div class="bar"><div role="tablist" aria-label="Release">
    <button type="button" role="tab" id="st-tab-1" aria-selected="true" aria-controls="st-panel-1">v4.8</button>
    <button type="button" role="tab" id="st-tab-2" aria-selected="false" aria-controls="st-panel-2" tabindex="-1">v4.7</button>
    <button type="button" role="tab" id="st-tab-3" aria-selected="false" aria-controls="st-panel-3" tabindex="-1">v4.6</button>
    <button type="button" role="tab" id="st-tab-4" aria-selected="false" aria-controls="st-panel-4" tabindex="-1">v4.5</button>
    <button type="button" role="tab" id="st-tab-5" aria-selected="false" aria-controls="st-panel-5" tabindex="-1">v4.4</button>
    <button type="button" role="tab" id="st-tab-6" aria-selected="false" aria-controls="st-panel-6" tabindex="-1">v4.3</button>
    <button type="button" role="tab" id="st-tab-7" aria-selected="false" aria-controls="st-panel-7" tabindex="-1">v4.2</button>
    <button type="button" role="tab" id="st-tab-8" aria-selected="false" aria-controls="st-panel-8" tabindex="-1">v4.1</button>
  </div></div>
  <div class="body">
    <div role="tabpanel" id="st-panel-1" aria-labelledby="st-tab-1" tabindex="0"><p>Notes for release <strong>v4.8</strong>. The current release — subgrid support landed and nested cards now align to the parent tracks.</p></div>
    <div role="tabpanel" id="st-panel-2" aria-labelledby="st-tab-2" tabindex="0" hidden><p>Notes for release <strong>v4.7</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div role="tabpanel" id="st-panel-3" aria-labelledby="st-tab-3" tabindex="0" hidden><p>Notes for release <strong>v4.6</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div role="tabpanel" id="st-panel-4" aria-labelledby="st-tab-4" tabindex="0" hidden><p>Notes for release <strong>v4.5</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div role="tabpanel" id="st-panel-5" aria-labelledby="st-tab-5" tabindex="0" hidden><p>Notes for release <strong>v4.4</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div role="tabpanel" id="st-panel-6" aria-labelledby="st-tab-6" tabindex="0" hidden><p>Notes for release <strong>v4.3</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div role="tabpanel" id="st-panel-7" aria-labelledby="st-tab-7" tabindex="0" hidden><p>Notes for release <strong>v4.2</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div role="tabpanel" id="st-panel-8" aria-labelledby="st-tab-8" tabindex="0" hidden><p>Notes for release <strong>v4.1</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
  </div>
</div>
CSS
/* Scrollable tab bar: the tabs sit in one row that scrolls sideways instead of wrapping, with fades at both edges.
   Focusing a tab (Tab key or arrows) scrolls it into view. */
.stabs{--st-accent:#4f46e5;--st-muted:#5d6a86;--st-text:#16203a;--st-card:#fff;
  box-sizing:border-box;width:100%;max-width:560px;padding:22px 0 26px;overflow:hidden;background:var(--st-card);border:1px solid #e3e8f2;
  border-radius:16px;box-shadow:0 16px 38px -24px rgba(16,24,48,.24);color:var(--st-text)}
.stabs .hd{padding:0 26px 16px}
.stabs .hd h2{margin:0;font-size:1rem}
.stabs .hd p{margin:3px 0 0;font-size:.82rem;color:#5f6b85}
.stabs .bar{position:relative}
.stabs .bar::before,.stabs .bar::after{content:"";position:absolute;top:0;bottom:8px;width:34px;z-index:2;pointer-events:none}
.stabs .bar::before{left:0;background:linear-gradient(90deg,var(--st-card),rgba(255,255,255,0))}
.stabs .bar::after{right:0;background:linear-gradient(270deg,var(--st-card),rgba(255,255,255,0))}
.stabs [role=tablist]{display:flex;gap:4px;overflow-x:auto;padding:0 26px 8px;scroll-padding-inline:34px;scrollbar-width:none;-webkit-overflow-scrolling:touch}
.stabs [role=tablist]::-webkit-scrollbar{display:none}
.stabs [role=tab]{flex:0 0 auto;margin:0;border:0;background:none;padding:9px 15px;border-radius:9px;font:inherit;font-size:.86rem;font-weight:700;
  color:var(--st-muted);cursor:pointer;white-space:nowrap;transition:background-color .15s,color .15s}
.stabs [role=tab]:hover{background:#f1f4fa;color:var(--st-text)}
.stabs [role=tab][aria-selected=true]{background:var(--st-accent);color:#fff}
.stabs .body{padding:18px 26px 0;border-top:1px solid #eef1f7;margin-top:8px}
.stabs [role=tabpanel]{border-radius:6px}
.stabs [role=tabpanel] p{margin:0;font-size:.9rem;line-height:1.7;color:#4a5670}
.stabs [role=tab]:focus-visible{outline:2px solid var(--st-accent);outline-offset:2px}
.stabs [role=tabpanel]:focus-visible{outline:2px solid var(--st-accent);outline-offset:4px}
.stabs [hidden]{display:none}
@media (prefers-reduced-motion:reduce){.stabs [role=tab]{transition:none}}
JavaScript
/* WAI-ARIA tabs: click or use the arrow keys to select, Home/End jump to the first/last tab.
   Only the selected tab is in the Tab order (roving tabindex); a clicked tab is scrolled fully into view. */
document.querySelectorAll('.stabs').forEach(root => {
  const list = root.querySelector('[role=tablist]');
  const tabs = [...list.querySelectorAll('[role=tab]')];
  const select = (tab, focus) => {
    tabs.forEach(t => {
      const on = t === tab;
      t.setAttribute('aria-selected', on);
      t.tabIndex = on ? 0 : -1;
      document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
    });
    if (focus) tab.focus();
    tab.scrollIntoView({ block: 'nearest', inline: 'nearest' });
  };
  list.addEventListener('click', e => { const t = e.target.closest('[role=tab]'); if (t) select(t); });
  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(tabs[j % n], true);
  });
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div class="stabs">
  <div class="hd"><h2>Release notes</h2><p>Eight releases — the bar scrolls rather than wrapping.</p></div>
  <div class="bar"><ul class="nav nav-pills flex-nowrap overflow-x-auto" role="tablist" aria-label="Release">
    <li class="nav-item" role="presentation"><button class="nav-link active" type="button" id="st-tab-1" data-bs-toggle="pill" data-bs-target="#st-panel-1" role="tab" aria-controls="st-panel-1" aria-selected="true">v4.8</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="st-tab-2" data-bs-toggle="pill" data-bs-target="#st-panel-2" role="tab" aria-controls="st-panel-2" aria-selected="false" tabindex="-1">v4.7</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="st-tab-3" data-bs-toggle="pill" data-bs-target="#st-panel-3" role="tab" aria-controls="st-panel-3" aria-selected="false" tabindex="-1">v4.6</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="st-tab-4" data-bs-toggle="pill" data-bs-target="#st-panel-4" role="tab" aria-controls="st-panel-4" aria-selected="false" tabindex="-1">v4.5</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="st-tab-5" data-bs-toggle="pill" data-bs-target="#st-panel-5" role="tab" aria-controls="st-panel-5" aria-selected="false" tabindex="-1">v4.4</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="st-tab-6" data-bs-toggle="pill" data-bs-target="#st-panel-6" role="tab" aria-controls="st-panel-6" aria-selected="false" tabindex="-1">v4.3</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="st-tab-7" data-bs-toggle="pill" data-bs-target="#st-panel-7" role="tab" aria-controls="st-panel-7" aria-selected="false" tabindex="-1">v4.2</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="st-tab-8" data-bs-toggle="pill" data-bs-target="#st-panel-8" role="tab" aria-controls="st-panel-8" aria-selected="false" tabindex="-1">v4.1</button></li>
  </ul></div>
  <div class="tab-content">
    <div class="tab-pane active" id="st-panel-1" role="tabpanel" aria-labelledby="st-tab-1" tabindex="0"><p>Notes for release <strong>v4.8</strong>. The current release — subgrid support landed and nested cards now align to the parent tracks.</p></div>
    <div class="tab-pane" id="st-panel-2" role="tabpanel" aria-labelledby="st-tab-2" tabindex="0"><p>Notes for release <strong>v4.7</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div class="tab-pane" id="st-panel-3" role="tabpanel" aria-labelledby="st-tab-3" tabindex="0"><p>Notes for release <strong>v4.6</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div class="tab-pane" id="st-panel-4" role="tabpanel" aria-labelledby="st-tab-4" tabindex="0"><p>Notes for release <strong>v4.5</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div class="tab-pane" id="st-panel-5" role="tabpanel" aria-labelledby="st-tab-5" tabindex="0"><p>Notes for release <strong>v4.4</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div class="tab-pane" id="st-panel-6" role="tabpanel" aria-labelledby="st-tab-6" tabindex="0"><p>Notes for release <strong>v4.3</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div class="tab-pane" id="st-panel-7" role="tabpanel" aria-labelledby="st-tab-7" tabindex="0"><p>Notes for release <strong>v4.2</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div class="tab-pane" id="st-panel-8" role="tabpanel" aria-labelledby="st-tab-8" tabindex="0"><p>Notes for release <strong>v4.1</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
  </div>
</div>
CSS
/* Bootstrap .nav-pills with the Tab plugin in a single row that scrolls sideways (.flex-nowrap, .overflow-x-auto), with fades at both edges. */
.stabs{--st-accent:#4f46e5;--st-muted:#5d6a86;--st-text:#16203a;--st-card:#fff;
  width:100%;max-width:560px;padding:22px 0 26px;overflow:hidden;background:var(--st-card);border:1px solid #e3e8f2;
  border-radius:16px;box-shadow:0 16px 38px -24px rgba(16,24,48,.24);color:var(--st-text)}
.stabs .hd{padding:0 26px 16px}
.stabs .hd h2{margin:0;font-size:1rem;font-weight:700;line-height:normal}
.stabs .hd p{margin:3px 0 0;font-size:.82rem;color:#5f6b85}
.stabs .bar{position:relative}
.stabs .bar::before,.stabs .bar::after{content:"";position:absolute;top:0;bottom:8px;width:34px;z-index:2;pointer-events:none}
.stabs .bar::before{left:0;background:linear-gradient(90deg,var(--st-card),rgba(255,255,255,0))}
.stabs .bar::after{right:0;background:linear-gradient(270deg,var(--st-card),rgba(255,255,255,0))}
.stabs .nav-pills{--bs-nav-link-padding-x:15px;--bs-nav-link-padding-y:9px;--bs-nav-link-font-size:.86rem;--bs-nav-link-font-weight:700;
  --bs-nav-link-color:var(--st-muted);--bs-nav-link-hover-color:var(--st-text);
  --bs-nav-pills-border-radius:9px;--bs-nav-pills-link-active-bg:var(--st-accent);--bs-nav-pills-link-active-color:#fff;
  gap:4px;padding:0 26px 8px;scroll-padding-inline:34px;scrollbar-width:none}
.stabs .nav-pills::-webkit-scrollbar{display:none}
.stabs .nav-item{flex:0 0 auto}
.stabs .nav-link{line-height:normal;white-space:nowrap;transition:background-color .15s,color .15s}
.stabs .nav-link:not(.active):hover{background:#f1f4fa}
.stabs .tab-content{padding:18px 26px 0;border-top:1px solid #eef1f7;margin-top:8px}
.stabs .tab-pane{border-radius:6px}
.stabs .tab-pane p{margin:0;font-size:.9rem;line-height:1.7;color:#4a5670}
.stabs .nav-link:focus-visible{box-shadow:none;outline:2px solid var(--st-accent);outline-offset:2px}
.stabs .tab-pane:focus-visible{outline:2px solid var(--st-accent);outline-offset:4px}
@media (prefers-reduced-motion:reduce){.stabs .nav-link{transition:none}}
JavaScript
/* The Tab plugin handles selection, arrow keys, Home/End and roving tabindex. This keeps the active pill scrolled into view. */
document.querySelectorAll('.stabs .nav').forEach(nav =>
  nav.addEventListener('shown.bs.tab', e => e.target.scrollIntoView({ block: 'nearest', inline: 'nearest' })));

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="stabs w-full max-w-[560px] overflow-hidden rounded-2xl border border-[#e3e8f2] bg-white pt-[22px] pb-[26px] text-st-text shadow-[0_16px_38px_-24px_rgba(16,24,48,.24)]">
  <div class="px-[26px] pb-4"><h2 class="text-[1rem] font-bold">Release notes</h2><p class="mt-[3px] text-[.82rem] text-[#5f6b85]">Eight releases — the bar scrolls rather than wrapping.</p></div>
  <div class="relative before:pointer-events-none before:absolute before:top-0 before:bottom-2 before:left-0 before:z-[2] before:w-[34px] before:bg-linear-to-r before:from-white before:to-white/0 after:pointer-events-none after:absolute after:top-0 after:right-0 after:bottom-2 after:z-[2] after:w-[34px] after:bg-linear-to-l after:from-white after:to-white/0"><div role="tablist" aria-label="Release" class="flex scroll-px-[34px] gap-1 overflow-x-auto px-[26px] pb-2 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden">
    <button type="button" role="tab" id="st-tab-1" aria-selected="true" aria-controls="st-panel-1" class="shrink-0 cursor-pointer rounded-[9px] px-[15px] py-[9px] text-[.86rem] font-bold whitespace-nowrap text-st-muted transition-colors duration-150 hover:bg-[#f1f4fa] hover:text-st-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent aria-selected:bg-st-accent aria-selected:text-white motion-reduce:transition-none">v4.8</button>
    <button type="button" role="tab" id="st-tab-2" aria-selected="false" aria-controls="st-panel-2" tabindex="-1" class="shrink-0 cursor-pointer rounded-[9px] px-[15px] py-[9px] text-[.86rem] font-bold whitespace-nowrap text-st-muted transition-colors duration-150 hover:bg-[#f1f4fa] hover:text-st-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent aria-selected:bg-st-accent aria-selected:text-white motion-reduce:transition-none">v4.7</button>
    <button type="button" role="tab" id="st-tab-3" aria-selected="false" aria-controls="st-panel-3" tabindex="-1" class="shrink-0 cursor-pointer rounded-[9px] px-[15px] py-[9px] text-[.86rem] font-bold whitespace-nowrap text-st-muted transition-colors duration-150 hover:bg-[#f1f4fa] hover:text-st-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent aria-selected:bg-st-accent aria-selected:text-white motion-reduce:transition-none">v4.6</button>
    <button type="button" role="tab" id="st-tab-4" aria-selected="false" aria-controls="st-panel-4" tabindex="-1" class="shrink-0 cursor-pointer rounded-[9px] px-[15px] py-[9px] text-[.86rem] font-bold whitespace-nowrap text-st-muted transition-colors duration-150 hover:bg-[#f1f4fa] hover:text-st-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent aria-selected:bg-st-accent aria-selected:text-white motion-reduce:transition-none">v4.5</button>
    <button type="button" role="tab" id="st-tab-5" aria-selected="false" aria-controls="st-panel-5" tabindex="-1" class="shrink-0 cursor-pointer rounded-[9px] px-[15px] py-[9px] text-[.86rem] font-bold whitespace-nowrap text-st-muted transition-colors duration-150 hover:bg-[#f1f4fa] hover:text-st-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent aria-selected:bg-st-accent aria-selected:text-white motion-reduce:transition-none">v4.4</button>
    <button type="button" role="tab" id="st-tab-6" aria-selected="false" aria-controls="st-panel-6" tabindex="-1" class="shrink-0 cursor-pointer rounded-[9px] px-[15px] py-[9px] text-[.86rem] font-bold whitespace-nowrap text-st-muted transition-colors duration-150 hover:bg-[#f1f4fa] hover:text-st-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent aria-selected:bg-st-accent aria-selected:text-white motion-reduce:transition-none">v4.3</button>
    <button type="button" role="tab" id="st-tab-7" aria-selected="false" aria-controls="st-panel-7" tabindex="-1" class="shrink-0 cursor-pointer rounded-[9px] px-[15px] py-[9px] text-[.86rem] font-bold whitespace-nowrap text-st-muted transition-colors duration-150 hover:bg-[#f1f4fa] hover:text-st-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent aria-selected:bg-st-accent aria-selected:text-white motion-reduce:transition-none">v4.2</button>
    <button type="button" role="tab" id="st-tab-8" aria-selected="false" aria-controls="st-panel-8" tabindex="-1" class="shrink-0 cursor-pointer rounded-[9px] px-[15px] py-[9px] text-[.86rem] font-bold whitespace-nowrap text-st-muted transition-colors duration-150 hover:bg-[#f1f4fa] hover:text-st-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-st-accent aria-selected:bg-st-accent aria-selected:text-white motion-reduce:transition-none">v4.1</button>
  </div></div>
  <div class="mt-2 border-t border-[#eef1f7] px-[26px] pt-[18px]">
    <div role="tabpanel" id="st-panel-1" aria-labelledby="st-tab-1" tabindex="0" class="rounded-md focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-st-accent"><p class="text-[.9rem] leading-[1.7] text-[#4a5670]">Notes for release <strong>v4.8</strong>. The current release — subgrid support landed and nested cards now align to the parent tracks.</p></div>
    <div role="tabpanel" id="st-panel-2" aria-labelledby="st-tab-2" tabindex="0" hidden class="rounded-md focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-st-accent"><p class="text-[.9rem] leading-[1.7] text-[#4a5670]">Notes for release <strong>v4.7</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div role="tabpanel" id="st-panel-3" aria-labelledby="st-tab-3" tabindex="0" hidden class="rounded-md focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-st-accent"><p class="text-[.9rem] leading-[1.7] text-[#4a5670]">Notes for release <strong>v4.6</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div role="tabpanel" id="st-panel-4" aria-labelledby="st-tab-4" tabindex="0" hidden class="rounded-md focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-st-accent"><p class="text-[.9rem] leading-[1.7] text-[#4a5670]">Notes for release <strong>v4.5</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div role="tabpanel" id="st-panel-5" aria-labelledby="st-tab-5" tabindex="0" hidden class="rounded-md focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-st-accent"><p class="text-[.9rem] leading-[1.7] text-[#4a5670]">Notes for release <strong>v4.4</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div role="tabpanel" id="st-panel-6" aria-labelledby="st-tab-6" tabindex="0" hidden class="rounded-md focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-st-accent"><p class="text-[.9rem] leading-[1.7] text-[#4a5670]">Notes for release <strong>v4.3</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div role="tabpanel" id="st-panel-7" aria-labelledby="st-tab-7" tabindex="0" hidden class="rounded-md focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-st-accent"><p class="text-[.9rem] leading-[1.7] text-[#4a5670]">Notes for release <strong>v4.2</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
    <div role="tabpanel" id="st-panel-8" aria-labelledby="st-tab-8" tabindex="0" hidden class="rounded-md focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-st-accent"><p class="text-[.9rem] leading-[1.7] text-[#4a5670]">Notes for release <strong>v4.1</strong>. Maintenance release: dependency bumps, two layout fixes and a corrected focus order in the drawer.</p></div>
  </div>
</div>
CSS
@theme {
  --color-st-accent: #4f46e5;
  --color-st-muted: #5d6a86;
  --color-st-text: #16203a;
}
JavaScript
/* WAI-ARIA tabs: click or use the arrow keys to select, Home/End jump to the first/last tab.
   Only the selected tab is in the Tab order (roving tabindex); a clicked tab is scrolled fully into view. */
document.querySelectorAll('.stabs').forEach(root => {
  const list = root.querySelector('[role=tablist]');
  const tabs = [...list.querySelectorAll('[role=tab]')];
  const select = (tab, focus) => {
    tabs.forEach(t => {
      const on = t === tab;
      t.setAttribute('aria-selected', on);
      t.tabIndex = on ? 0 : -1;
      document.getElementById(t.getAttribute('aria-controls')).hidden = !on;
    });
    if (focus) tab.focus();
    tab.scrollIntoView({ block: 'nearest', inline: 'nearest' });
  };
  list.addEventListener('click', e => { const t = e.target.closest('[role=tab]'); if (t) select(t); });
  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(tabs[j % n], true);
  });
});

About this set

A Release notes card with eight version tabs, v4.8 down to v4.1, in a single row that scrolls sideways instead of wrapping, with white fades at both edges to hint that more tabs are hidden. The selected version fills with indigo and the notes for that release appear below a divider. Use it when the number of tabs is open-ended: versions, dates, categories, team members or product variants, especially on phones where eight tabs never fit. The row keeps its own horizontal scroll with the scrollbar hidden, and scroll-padding keeps the selected tab clear of the edge fades. Moving with the arrow keys focuses the next tab, which the browser scrolls into view, and a clicked tab is scrolled fully into view as well, so the selected tab is never left half hidden. The script sets aria-selected, the roving tabindex and the hidden panels. The Bootstrap version is .nav-pills with the .flex-nowrap and .overflow-x-auto utilities and the Tab plugin, and the Tailwind version draws the fades with before and after utilities.

What’s included

  • Single scrolling row that never wraps
  • Edge fades hint at hidden tabs
  • Selected and focused tabs scrolled into view
  • Scroll padding keeps tabs clear of the fades
  • Converted from radio inputs to real ARIA tabs

Accessibility

The versions are tabs in a tablist labelled Release, each controlling a panel labelled by its tab. 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. Because the focused tab is a real button inside the scroller, keyboard focus always scrolls into view, and it is drawn as a 2px indigo outline. Colour transitions are removed under prefers-reduced-motion.

Customise it

Change --st-accent, --st-muted, --st-text and --st-card on .stabs (plain CSS and Bootstrap); keep --st-card equal to the card background so the fades blend. Bootstrap also takes --bs-nav-pills-link-active-bg and the --bs-nav-link-* variables. In Tailwind edit the --color-st-* theme colours.