Open demo

CSS code

HTML
<div class="ptabs">
  <div role="tablist" aria-label="Billing period">
    <span class="thumb" aria-hidden="true"></span>
    <button type="button" role="tab" id="pt-tab-0" aria-selected="true" aria-controls="pt-panel-0">Monthly</button>
    <button type="button" role="tab" id="pt-tab-1" aria-selected="false" aria-controls="pt-panel-1" tabindex="-1">Yearly</button>
    <button type="button" role="tab" id="pt-tab-2" aria-selected="false" aria-controls="pt-panel-2" tabindex="-1">Lifetime</button>
  </div>
  <div role="tabpanel" id="pt-panel-0" aria-labelledby="pt-tab-0" tabindex="0"><b>$19</b><p>per month, billed monthly</p></div>
  <div role="tabpanel" id="pt-panel-1" aria-labelledby="pt-tab-1" tabindex="0" hidden><b>$15</b><p>per month, billed yearly — save 21%</p></div>
  <div role="tabpanel" id="pt-panel-2" aria-labelledby="pt-tab-2" tabindex="0" hidden><b>$390</b><p>once, yours forever</p></div>
</div>
CSS
/* Segmented pill tabs: a light .thumb slides behind the selected tab; each tab controls one price card. */
.ptabs{--pt-bg:#1a1f29;--pt-border:#262d3b;--pt-muted:#8d97ab;--pt-thumb:#e8ebf2;--pt-on:#101318;--pt-focus:#60a5fa;
  box-sizing:border-box;width:100%;display:flex;flex-direction:column;align-items:center;gap:26px;color:#e8ebf2}
.ptabs [role=tablist]{position:relative;display:flex;background:var(--pt-bg);border:1px solid var(--pt-border);border-radius:13px;padding:5px}
.ptabs [role=tab]{position:relative;z-index:1;border:0;margin:0;background:none;color:var(--pt-muted);font:inherit;font-size:.88rem;font-weight:700;
  padding:9px 22px;border-radius:9px;cursor:pointer;transition:color .2s}
.ptabs [role=tab]:hover{color:#c9d0dd}
.ptabs [role=tab][aria-selected=true]{color:var(--pt-on)}
.ptabs .thumb{position:absolute;top:5px;bottom:5px;left:5px;width:0;background:var(--pt-thumb);border-radius:9px;
  box-shadow:0 4px 14px rgba(0,0,0,.35);transition:left .25s cubic-bezier(.4,.1,.2,1),width .25s cubic-bezier(.4,.1,.2,1)}
.ptabs [role=tabpanel]{box-sizing:border-box;width:100%;max-width:460px;background:#161b24;border:1px solid #232a37;border-radius:16px;padding:26px;text-align:center}
.ptabs [role=tabpanel] b{display:block;font-size:1.6rem;letter-spacing:-.02em}
.ptabs [role=tabpanel] p{margin:6px 0 0;color:var(--pt-muted);font-size:.86rem}
.ptabs [role=tab]:focus-visible,.ptabs [role=tabpanel]:focus-visible{outline:2px solid var(--pt-focus);outline-offset:2px}
.ptabs [hidden]{display:none}
@media (prefers-reduced-motion:reduce){.ptabs [role=tab],.ptabs .thumb{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). The thumb follows the selected tab. */
document.querySelectorAll('.ptabs').forEach(root => {
  const list = root.querySelector('[role=tablist]'), thumb = root.querySelector('.thumb');
  const tabs = [...list.querySelectorAll('[role=tab]')];
  const current = () => tabs.find(t => t.getAttribute('aria-selected') === 'true');
  const place = t => { thumb.style.left = t.offsetLeft + 'px'; thumb.style.width = t.offsetWidth + 'px'; };
  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();
    place(tab);
  };
  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);
  });
  thumb.style.transition = 'none'; place(current()); thumb.offsetWidth; thumb.style.transition = '';
  new ResizeObserver(() => place(current())).observe(list);
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div class="ptabs">
  <ul class="nav nav-pills" role="tablist" aria-label="Billing period">
    <li class="thumb" role="presentation" aria-hidden="true"></li>
    <li class="nav-item" role="presentation"><button class="nav-link active" type="button" id="pt-tab-0" data-bs-toggle="pill" data-bs-target="#pt-panel-0" role="tab" aria-controls="pt-panel-0" aria-selected="true">Monthly</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="pt-tab-1" data-bs-toggle="pill" data-bs-target="#pt-panel-1" role="tab" aria-controls="pt-panel-1" aria-selected="false" tabindex="-1">Yearly</button></li>
    <li class="nav-item" role="presentation"><button class="nav-link" type="button" id="pt-tab-2" data-bs-toggle="pill" data-bs-target="#pt-panel-2" role="tab" aria-controls="pt-panel-2" aria-selected="false" tabindex="-1">Lifetime</button></li>
  </ul>
  <div class="tab-content">
    <div class="tab-pane active" id="pt-panel-0" role="tabpanel" aria-labelledby="pt-tab-0" tabindex="0"><b>$19</b><p>per month, billed monthly</p></div>
    <div class="tab-pane" id="pt-panel-1" role="tabpanel" aria-labelledby="pt-tab-1" tabindex="0"><b>$15</b><p>per month, billed yearly — save 21%</p></div>
    <div class="tab-pane" id="pt-panel-2" role="tabpanel" aria-labelledby="pt-tab-2" tabindex="0"><b>$390</b><p>once, yours forever</p></div>
  </div>
</div>
CSS
/* Bootstrap .nav-pills with the Tab plugin. The active pill background is made transparent and a .thumb slides behind it instead. */
.ptabs{--pt-bg:#1a1f29;--pt-border:#262d3b;--pt-muted:#8d97ab;--pt-thumb:#e8ebf2;--pt-on:#101318;--pt-focus:#60a5fa;
  width:100%;display:flex;flex-direction:column;align-items:center;gap:26px;color:#e8ebf2}
.ptabs .nav-pills{--bs-nav-link-padding-x:22px;--bs-nav-link-padding-y:9px;--bs-nav-link-font-size:.88rem;--bs-nav-link-font-weight:700;
  --bs-nav-link-color:var(--pt-muted);--bs-nav-link-hover-color:#c9d0dd;
  --bs-nav-pills-border-radius:9px;--bs-nav-pills-link-active-bg:transparent;--bs-nav-pills-link-active-color:var(--pt-on);
  position:relative;flex-wrap:nowrap;background:var(--pt-bg);border:1px solid var(--pt-border);border-radius:13px;padding:5px}
.ptabs .nav-link{position:relative;z-index:1;line-height:normal;transition:color .2s}
.ptabs .thumb{position:absolute;top:5px;bottom:5px;left:5px;width:0;background:var(--pt-thumb);border-radius:9px;
  box-shadow:0 4px 14px rgba(0,0,0,.35);transition:left .25s cubic-bezier(.4,.1,.2,1),width .25s cubic-bezier(.4,.1,.2,1)}
.ptabs .tab-content{width:100%;max-width:460px}
.ptabs .tab-pane{background:#161b24;border:1px solid #232a37;border-radius:16px;padding:26px;text-align:center}
.ptabs .tab-pane b{display:block;font-size:1.6rem;font-weight:700;letter-spacing:-.02em}
.ptabs .tab-pane p{margin:6px 0 0;color:var(--pt-muted);font-size:.86rem}
.ptabs .nav-link:focus-visible,.ptabs .tab-pane:focus-visible{box-shadow:none;outline:2px solid var(--pt-focus);outline-offset:2px}
@media (prefers-reduced-motion:reduce){.ptabs .nav-link,.ptabs .thumb{transition:none}}
JavaScript
/* The Tab plugin handles selection, arrow keys, Home/End and roving tabindex. This only moves the thumb. */
document.querySelectorAll('.ptabs').forEach(root => {
  const nav = root.querySelector('.nav'), thumb = root.querySelector('.thumb');
  const place = t => { thumb.style.left = t.offsetLeft + 'px'; thumb.style.width = t.offsetWidth + 'px'; };
  nav.addEventListener('show.bs.tab', e => place(e.target));
  thumb.style.transition = 'none'; place(nav.querySelector('.nav-link.active')); thumb.offsetWidth; thumb.style.transition = '';
  new ResizeObserver(() => place(nav.querySelector('.nav-link.active'))).observe(nav);
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="ptabs flex w-full flex-col items-center gap-[26px] text-[#e8ebf2]">
  <div role="tablist" aria-label="Billing period" class="relative flex rounded-[13px] border border-pt-border bg-pt-bg p-[5px]">
    <span class="thumb absolute top-[5px] bottom-[5px] left-[5px] w-0 rounded-[9px] bg-pt-thumb shadow-[0_4px_14px_rgba(0,0,0,.35)] transition-[left,width] duration-[250ms] ease-[cubic-bezier(.4,.1,.2,1)] motion-reduce:transition-none" aria-hidden="true"></span>
    <button type="button" role="tab" id="pt-tab-0" aria-selected="true" aria-controls="pt-panel-0" class="relative z-[1] cursor-pointer rounded-[9px] px-[22px] py-[9px] text-[.88rem] font-bold text-pt-muted transition-colors duration-200 hover:text-[#c9d0dd] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pt-focus aria-selected:text-pt-on motion-reduce:transition-none">Monthly</button>
    <button type="button" role="tab" id="pt-tab-1" aria-selected="false" aria-controls="pt-panel-1" tabindex="-1" class="relative z-[1] cursor-pointer rounded-[9px] px-[22px] py-[9px] text-[.88rem] font-bold text-pt-muted transition-colors duration-200 hover:text-[#c9d0dd] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pt-focus aria-selected:text-pt-on motion-reduce:transition-none">Yearly</button>
    <button type="button" role="tab" id="pt-tab-2" aria-selected="false" aria-controls="pt-panel-2" tabindex="-1" class="relative z-[1] cursor-pointer rounded-[9px] px-[22px] py-[9px] text-[.88rem] font-bold text-pt-muted transition-colors duration-200 hover:text-[#c9d0dd] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pt-focus aria-selected:text-pt-on motion-reduce:transition-none">Lifetime</button>
  </div>
  <div role="tabpanel" id="pt-panel-0" aria-labelledby="pt-tab-0" tabindex="0" class="w-full max-w-[460px] rounded-2xl border border-[#232a37] bg-[#161b24] p-[26px] text-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pt-focus"><b class="block text-[1.6rem] tracking-[-.02em]">$19</b><p class="mt-1.5 text-[.86rem] text-pt-muted">per month, billed monthly</p></div>
  <div role="tabpanel" id="pt-panel-1" aria-labelledby="pt-tab-1" tabindex="0" hidden class="w-full max-w-[460px] rounded-2xl border border-[#232a37] bg-[#161b24] p-[26px] text-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pt-focus"><b class="block text-[1.6rem] tracking-[-.02em]">$15</b><p class="mt-1.5 text-[.86rem] text-pt-muted">per month, billed yearly — save 21%</p></div>
  <div role="tabpanel" id="pt-panel-2" aria-labelledby="pt-tab-2" tabindex="0" hidden class="w-full max-w-[460px] rounded-2xl border border-[#232a37] bg-[#161b24] p-[26px] text-center focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-pt-focus"><b class="block text-[1.6rem] tracking-[-.02em]">$390</b><p class="mt-1.5 text-[.86rem] text-pt-muted">once, yours forever</p></div>
</div>
CSS
@theme {
  --color-pt-bg: #1a1f29;
  --color-pt-border: #262d3b;
  --color-pt-muted: #8d97ab;
  --color-pt-thumb: #e8ebf2;
  --color-pt-on: #101318;
  --color-pt-focus: #60a5fa;
}
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). The thumb follows the selected tab. */
document.querySelectorAll('.ptabs').forEach(root => {
  const list = root.querySelector('[role=tablist]'), thumb = root.querySelector('.thumb');
  const tabs = [...list.querySelectorAll('[role=tab]')];
  const current = () => tabs.find(t => t.getAttribute('aria-selected') === 'true');
  const place = t => { thumb.style.left = t.offsetLeft + 'px'; thumb.style.width = t.offsetWidth + 'px'; };
  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();
    place(tab);
  };
  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);
  });
  thumb.style.transition = 'none'; place(current()); thumb.offsetWidth; thumb.style.transition = '';
  new ResizeObserver(() => place(current())).observe(list);
});

About this set

A dark segmented control with three options, Monthly, Yearly and Lifetime, where a light pill slides behind the selected option, and a price card underneath that changes with it: 19 dollars a month, 15 dollars a month billed yearly, or 390 dollars once. It is the billing-period switcher found on pricing pages, but the same pattern works for any two to four views that share one card, such as Day, Week and Month on a chart. Each option is a tab and each price card is its own tabpanel, so the content is real markup rather than values copied from data attributes. The script sets aria-selected, the roving tabindex and the hidden panels, and moves the pill by reading the selected tab’s offsetLeft and offsetWidth, with a ResizeObserver keeping it aligned. In the Bootstrap version the options are .nav-pills run by the Tab plugin with the active background made transparent so the sliding pill shows through; the Tailwind version uses aria-selected utilities.

What’s included

  • Light pill slides behind the selected option
  • Each price card is a real tabpanel
  • Designed for dark backgrounds, with a light blue focus ring
  • Works for two to four options
  • Pill movement disabled under reduced motion

Accessibility

The options are tabs in a tablist labelled Billing period, and each controls a price 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. Keyboard focus draws a 2px light blue outline that stands out on the dark surface, and the pill and colour transitions are removed under prefers-reduced-motion.

Customise it

Colours are --pt-bg, --pt-border, --pt-muted, --pt-thumb, --pt-on and --pt-focus on .ptabs in plain CSS and Bootstrap, where the pill padding comes from --bs-nav-link-padding-x and -y. In Tailwind edit the --color-pt-* theme colours and the px-[22px] and py-[9px] utilities.