Open demo

CSS code

HTML
<div class="w">
  <div class="tabs" role="radiogroup" aria-label="Metric">
    <input type="radio" name="g" id="g1" checked><label for="g1"><span class="k">Revenue</span><span class="v">$48.2k</span><span class="d"><span aria-hidden="true">▲</span><span class="sr-only">up</span> 12.4%</span></label>
    <input type="radio" name="g" id="g2"><label for="g2"><span class="k">Active users</span><span class="v">18,442</span><span class="d"><span aria-hidden="true">▲</span><span class="sr-only">up</span> 3.1%</span></label>
    <input type="radio" name="g" id="g3"><label for="g3"><span class="k">Churn</span><span class="v">2.8%</span><span class="d down"><span aria-hidden="true">▼</span><span class="sr-only">down</span> 0.4pt</span></label>
  </div>
  <div class="body">
    <div class="p p1"><h3>Revenue</h3><p>Up 12.4% on last month, with most of the gain from annual upgrades rather than new seats.</p>
      <div class="spark" aria-hidden="true"><i style="height:35%"></i><i style="height:48%"></i><i style="height:42%"></i><i style="height:60%"></i><i style="height:55%"></i><i style="height:74%"></i><i style="height:92%"></i></div></div>
    <div class="p p2"><h3>Active users</h3><p>Steady growth, no single spike. Weekly actives are tracking monthly closely, which is usually the healthy shape.</p>
      <div class="spark" aria-hidden="true"><i style="height:55%"></i><i style="height:58%"></i><i style="height:50%"></i><i style="height:64%"></i><i style="height:61%"></i><i style="height:70%"></i><i style="height:76%"></i></div></div>
    <div class="p p3"><h3>Churn</h3><p>Down 0.4 points. The drop follows the onboarding checklist rewrite, though one month is not yet a trend.</p>
      <div class="spark" aria-hidden="true"><i style="height:80%"></i><i style="height:72%"></i><i style="height:75%"></i><i style="height:60%"></i><i style="height:52%"></i><i style="height:45%"></i><i style="height:38%"></i></div></div>
  </div>
</div>
CSS
/* Metric tabs, CSS only: each tab is a radio label showing a KPI, its value and the change.
   :has() shows the panel for the checked radio. The grid stacks into rows under 520px. */
.w{width:100%;max-width:600px;padding:6px 6px 26px;background:#fff;border:1px solid #e3e8f2;border-radius:16px;
  box-shadow:0 16px 38px -24px rgba(16,24,48,.24)}
.tabs{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;position:relative}
.tabs input{position:absolute;opacity:0;width:1px;height:1px;margin:0;pointer-events:none}
.tabs label{padding:16px 18px;border-radius:12px;cursor:pointer;transition:background-color .15s}
.tabs label:hover{background:#f4f6fb}
.tabs .k{display:block;font-size:.74rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:#5d6a86}
.tabs .v{display:block;font-size:1.5rem;font-weight:800;letter-spacing:-.02em;margin-top:5px}
.tabs .d{display:block;font-size:.75rem;font-weight:700;margin-top:3px;color:#166534}
.tabs .d.down{color:#b91c1c}
.tabs input:checked+label{background:#eef2ff}
.tabs input:checked+label .k{color:#4f46e5}
.tabs input:focus-visible+label{outline:2px solid #4f46e5;outline-offset:2px}
@media (max-width:520px){.tabs{grid-template-columns:1fr}
  .tabs label{display:flex;align-items:baseline;gap:10px;padding:11px 14px}
  .tabs .v{font-size:1.15rem;margin-top:0}.tabs .d{margin-top:0}}
.body{padding:20px 20px 0;margin:6px 14px 0;border-top:1px solid #eef1f7}
.body .p{display:none;font-size:.9rem;line-height:1.7;color:#4a5670}
.body h3{font-size:.94rem;color:#16203a;margin:0 0 6px}
.body p{margin:0}
.spark{display:flex;align-items:flex-end;gap:4px;height:52px;margin-top:14px}
.spark i{flex:1;border-radius:3px 3px 0 0;background:#e3e8f2}
.spark i:last-child{background:#4f46e5}
.w:has(#g1:checked) .p1,.w:has(#g2:checked) .p2,.w:has(#g3:checked) .p3{display:block}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media (prefers-reduced-motion:reduce){.tabs label{transition:none}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS, Bootstrap 5.3.8 JS bundle

HTML
<div class="w">
  <div class="nav nav-pills tabs" role="tablist" aria-label="Metric">
    <button class="nav-link active" id="kpi-revenue-tab" data-bs-toggle="pill" data-bs-target="#kpi-revenue" type="button" role="tab" aria-controls="kpi-revenue" aria-selected="true"><span class="k">Revenue</span><span class="v">$48.2k</span><span class="d"><span aria-hidden="true">▲</span><span class="visually-hidden">up</span> 12.4%</span></button>
    <button class="nav-link" id="kpi-users-tab" data-bs-toggle="pill" data-bs-target="#kpi-users" type="button" role="tab" aria-controls="kpi-users" aria-selected="false" tabindex="-1"><span class="k">Active users</span><span class="v">18,442</span><span class="d"><span aria-hidden="true">▲</span><span class="visually-hidden">up</span> 3.1%</span></button>
    <button class="nav-link" id="kpi-churn-tab" data-bs-toggle="pill" data-bs-target="#kpi-churn" type="button" role="tab" aria-controls="kpi-churn" aria-selected="false" tabindex="-1"><span class="k">Churn</span><span class="v">2.8%</span><span class="d down"><span aria-hidden="true">▼</span><span class="visually-hidden">down</span> 0.4pt</span></button>
  </div>
  <div class="tab-content body">
    <div class="tab-pane active" id="kpi-revenue" role="tabpanel" aria-labelledby="kpi-revenue-tab" tabindex="0"><h3>Revenue</h3><p>Up 12.4% on last month, with most of the gain from annual upgrades rather than new seats.</p>
      <div class="spark" aria-hidden="true"><i style="height:35%"></i><i style="height:48%"></i><i style="height:42%"></i><i style="height:60%"></i><i style="height:55%"></i><i style="height:74%"></i><i style="height:92%"></i></div></div>
    <div class="tab-pane" id="kpi-users" role="tabpanel" aria-labelledby="kpi-users-tab" tabindex="0"><h3>Active users</h3><p>Steady growth, no single spike. Weekly actives are tracking monthly closely, which is usually the healthy shape.</p>
      <div class="spark" aria-hidden="true"><i style="height:55%"></i><i style="height:58%"></i><i style="height:50%"></i><i style="height:64%"></i><i style="height:61%"></i><i style="height:70%"></i><i style="height:76%"></i></div></div>
    <div class="tab-pane" id="kpi-churn" role="tabpanel" aria-labelledby="kpi-churn-tab" tabindex="0"><h3>Churn</h3><p>Down 0.4 points. The drop follows the onboarding checklist rewrite, though one month is not yet a trend.</p>
      <div class="spark" aria-hidden="true"><i style="height:80%"></i><i style="height:72%"></i><i style="height:75%"></i><i style="height:60%"></i><i style="height:52%"></i><i style="height:45%"></i><i style="height:38%"></i></div></div>
  </div>
</div>
CSS
/* Metric tabs: a Bootstrap .nav-pills laid out as a grid, driven by the Tab plugin.
   Each .nav-link carries a KPI, its value and the change; the grid stacks into rows under 520px. */
.w{width:100%;max-width:600px;padding:6px 6px 26px;background:#fff;border:1px solid #e3e8f2;border-radius:16px;
  box-shadow:0 16px 38px -24px rgba(16,24,48,.24)}
.tabs{--bs-nav-link-padding-x:18px;--bs-nav-link-padding-y:16px;--bs-nav-link-color:#16203a;--bs-nav-link-hover-color:#16203a;
  --bs-nav-pills-border-radius:12px;--bs-nav-pills-link-active-bg:#eef2ff;--bs-nav-pills-link-active-color:#16203a;
  display:grid;grid-template-columns:repeat(3,1fr);gap:6px}
.tabs .nav-link{text-align:left;line-height:normal}
.tabs .nav-link:hover{background:#f4f6fb}
.tabs .nav-link.active{background:var(--bs-nav-pills-link-active-bg)}
.tabs .nav-link:focus-visible{box-shadow:none;outline:2px solid #4f46e5;outline-offset:2px}
.tabs .k{display:block;font-size:.74rem;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:#5d6a86}
.tabs .active .k{color:#4f46e5}
.tabs .v{display:block;font-size:1.5rem;font-weight:800;letter-spacing:-.02em;margin-top:5px}
.tabs .d{display:block;font-size:.75rem;font-weight:700;margin-top:3px;color:#166534}
.tabs .d.down{color:#b91c1c}
@media (max-width:520px){.tabs{grid-template-columns:1fr}
  .tabs .nav-link{display:flex;align-items:baseline;gap:10px;--bs-nav-link-padding-x:14px;--bs-nav-link-padding-y:11px}
  .tabs .v{font-size:1.15rem;margin-top:0}.tabs .d{margin-top:0}}
.body{padding:20px 20px 0;margin:6px 14px 0;border-top:1px solid #eef1f7}
.body .tab-pane{font-size:.9rem;line-height:1.7;color:#4a5670}
.body .tab-pane:focus-visible{outline:2px solid #4f46e5;outline-offset:4px;border-radius:4px}
.body h3{font-size:.94rem;font-weight:700;line-height:inherit;color:#16203a;margin:0 0 6px}
.body p{margin:0}
.spark{display:flex;align-items:flex-end;gap:4px;height:52px;margin-top:14px}
.spark i{flex:1;border-radius:3px 3px 0 0;background:#e3e8f2}
.spark i:last-child{background:#4f46e5}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div class="w-full max-w-[600px] rounded-2xl border border-tab-line bg-white px-1.5 pt-1.5 pb-[26px] shadow-[0_16px_38px_-24px_rgba(16,24,48,.24)]">
  <div class="grid grid-cols-3 gap-1.5 max-[520px]:grid-cols-1" role="tablist" aria-label="Metric">
    <button type="button" role="tab" id="kpi-revenue-tab" aria-controls="kpi-revenue" aria-selected="true" class="group cursor-pointer rounded-xl px-[18px] py-4 text-left transition-[color,background-color] duration-150 hover:bg-tab-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab-accent aria-selected:bg-tab-tint motion-reduce:transition-none max-[520px]:flex max-[520px]:items-baseline max-[520px]:gap-2.5 max-[520px]:px-3.5 max-[520px]:py-[11px]"><span class="block text-[.74rem] font-extrabold tracking-[.06em] text-tab-muted uppercase group-aria-selected:text-tab-accent">Revenue</span><span class="mt-[5px] block text-2xl font-extrabold tracking-[-.02em] max-[520px]:mt-0 max-[520px]:text-[1.15rem]">$48.2k</span><span class="mt-[3px] block text-[.75rem] font-bold text-tab-up max-[520px]:mt-0"><span aria-hidden="true">▲</span><span class="sr-only">up</span> 12.4%</span></button>
    <button type="button" role="tab" id="kpi-users-tab" aria-controls="kpi-users" aria-selected="false" tabindex="-1" class="group cursor-pointer rounded-xl px-[18px] py-4 text-left transition-[color,background-color] duration-150 hover:bg-tab-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab-accent aria-selected:bg-tab-tint motion-reduce:transition-none max-[520px]:flex max-[520px]:items-baseline max-[520px]:gap-2.5 max-[520px]:px-3.5 max-[520px]:py-[11px]"><span class="block text-[.74rem] font-extrabold tracking-[.06em] text-tab-muted uppercase group-aria-selected:text-tab-accent">Active users</span><span class="mt-[5px] block text-2xl font-extrabold tracking-[-.02em] max-[520px]:mt-0 max-[520px]:text-[1.15rem]">18,442</span><span class="mt-[3px] block text-[.75rem] font-bold text-tab-up max-[520px]:mt-0"><span aria-hidden="true">▲</span><span class="sr-only">up</span> 3.1%</span></button>
    <button type="button" role="tab" id="kpi-churn-tab" aria-controls="kpi-churn" aria-selected="false" tabindex="-1" class="group cursor-pointer rounded-xl px-[18px] py-4 text-left transition-[color,background-color] duration-150 hover:bg-tab-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tab-accent aria-selected:bg-tab-tint motion-reduce:transition-none max-[520px]:flex max-[520px]:items-baseline max-[520px]:gap-2.5 max-[520px]:px-3.5 max-[520px]:py-[11px]"><span class="block text-[.74rem] font-extrabold tracking-[.06em] text-tab-muted uppercase group-aria-selected:text-tab-accent">Churn</span><span class="mt-[5px] block text-2xl font-extrabold tracking-[-.02em] max-[520px]:mt-0 max-[520px]:text-[1.15rem]">2.8%</span><span class="mt-[3px] block text-[.75rem] font-bold text-tab-down max-[520px]:mt-0"><span aria-hidden="true">▼</span><span class="sr-only">down</span> 0.4pt</span></button>
  </div>
  <div class="mx-3.5 mt-1.5 border-t border-[#eef1f7] px-5 pt-5 text-[.9rem] leading-[1.7] text-tab-body">
    <div id="kpi-revenue" role="tabpanel" aria-labelledby="kpi-revenue-tab" tabindex="0" class="rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab-accent"><h3 class="mb-1.5 text-[.94rem] font-bold text-tab-ink">Revenue</h3><p>Up 12.4% on last month, with most of the gain from annual upgrades rather than new seats.</p>
      <div class="mt-3.5 flex h-[52px] items-end gap-1 *:flex-1 *:rounded-t-[3px] *:bg-tab-line *:last:bg-tab-accent" aria-hidden="true"><i class="h-[35%]"></i><i class="h-[48%]"></i><i class="h-[42%]"></i><i class="h-[60%]"></i><i class="h-[55%]"></i><i class="h-[74%]"></i><i class="h-[92%]"></i></div></div>
    <div id="kpi-users" role="tabpanel" aria-labelledby="kpi-users-tab" tabindex="0" hidden class="rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab-accent"><h3 class="mb-1.5 text-[.94rem] font-bold text-tab-ink">Active users</h3><p>Steady growth, no single spike. Weekly actives are tracking monthly closely, which is usually the healthy shape.</p>
      <div class="mt-3.5 flex h-[52px] items-end gap-1 *:flex-1 *:rounded-t-[3px] *:bg-tab-line *:last:bg-tab-accent" aria-hidden="true"><i class="h-[55%]"></i><i class="h-[58%]"></i><i class="h-[50%]"></i><i class="h-[64%]"></i><i class="h-[61%]"></i><i class="h-[70%]"></i><i class="h-[76%]"></i></div></div>
    <div id="kpi-churn" role="tabpanel" aria-labelledby="kpi-churn-tab" tabindex="0" hidden class="rounded focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab-accent"><h3 class="mb-1.5 text-[.94rem] font-bold text-tab-ink">Churn</h3><p>Down 0.4 points. The drop follows the onboarding checklist rewrite, though one month is not yet a trend.</p>
      <div class="mt-3.5 flex h-[52px] items-end gap-1 *:flex-1 *:rounded-t-[3px] *:bg-tab-line *:last:bg-tab-accent" aria-hidden="true"><i class="h-[80%]"></i><i class="h-[72%]"></i><i class="h-[75%]"></i><i class="h-[60%]"></i><i class="h-[52%]"></i><i class="h-[45%]"></i><i class="h-[38%]"></i></div></div>
  </div>
</div>
CSS
@theme {
  --color-tab-ink: #16203a;
  --color-tab-body: #4a5670;
  --color-tab-muted: #5d6a86;
  --color-tab-line: #e3e8f2;
  --color-tab-hover: #f4f6fb;
  --color-tab-tint: #eef2ff;
  --color-tab-accent: #4f46e5;
  --color-tab-up: #166534;
  --color-tab-down: #b91c1c;
}
JavaScript
// WAI-ARIA tabs: click or arrow keys select a tab, Home/End jump to the ends, only the selected tab is in the Tab order.
for (const list of document.querySelectorAll('[role=tablist]')) {
  const tabs = [...list.querySelectorAll('[role=tab]')];
  const select = tab => 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;
  });
  list.addEventListener('click', e => { const t = e.target.closest('[role=tab]'); if (t && !t.disabled) select(t); });
  list.addEventListener('keydown', e => {
    const live = tabs.filter(t => !t.disabled), i = live.indexOf(e.target);
    const n = { ArrowLeft: i - 1, ArrowUp: i - 1, ArrowRight: i + 1, ArrowDown: i + 1, Home: 0, End: live.length - 1 }[e.key];
    if (n === undefined || i < 0) return;
    e.preventDefault();
    const t = live[(n + live.length) % live.length];
    t.focus();
    select(t);
  });
}

About this set

Dashboard tabs where each tab is a small KPI card: a label, a large value and the change since last period, with Revenue, Active users and Churn as the examples. Selecting a card opens a panel below with a short explanation and a seven-bar sparkline whose last bar is highlighted. Use it at the top of an analytics or admin page, where people want the headline numbers at a glance and the detail one click away. On screens under 520 pixels the three cards stack into compact rows with the value and change on one line. The change arrows are hidden from screen readers and replaced by the words up or down, so a tab reads as Revenue, $48.2k, up 12.4%. The plain CSS version is a radio group with :has() choosing the panel; the Bootstrap version is a .nav-pills laid out as a grid and driven by the Tab plugin; the Tailwind version uses group-aria-selected utilities and a small WAI-ARIA tabs script.

What’s included

  • Each tab shows a KPI label, value and change
  • Arrows are read as up or down, not as symbols
  • Sparkline panel with the latest bar highlighted
  • Cards stack into rows under 520px
  • Green and red change colours darkened to pass 4.5:1 on the selected tint

Accessibility

In the plain CSS version the tabs are a radio group, not ARIA tabs: the radios sit inside an element with role="radiogroup" and an aria-label, each takes its name from its visible label, Tab enters the group and the arrow keys move the selection (radios have no Home or End). In the Bootstrap and Tailwind versions they follow the WAI-ARIA tabs pattern: role tablist, tab and tabpanel, aria-selected, aria-controls, a roving tabindex, arrow keys plus Home and End. The sparklines are aria-hidden because the panel text states the trend. Focus shows a 2px indigo outline and the hover transition is removed under prefers-reduced-motion.

Customise it

The selected tint #eef2ff and accent #4f46e5 live in the .tabs rules (plain CSS), in --bs-nav-pills-link-active-bg on .tabs (Bootstrap) and in the --color-tab-tint and --color-tab-accent theme colours (Tailwind). Change the up and down colours in .d and .d.down or --color-tab-up and --color-tab-down.