Pricing — Feature comparison matrix
A full feature-comparison matrix for a fictional SaaS, Northwind, with four plan columns (Starter, Pro, Scale, Enterprise) and fifteen features grouped into Core, Collaboration, Security and Support sections. Cells render inline check and cross SVG marks or specific values like 10 GB and Unlimited. A sticky header keeps plan names, prices and CTAs in view, a monthly to annual switch retunes every price, you can highlight one plan to dim the rest, collapse any feature group, and the first column stays pinned during horizontal scroll on mobile.
MCP
Code
:root {
--brand: #5b5bf0;
--brand-d: #4646d6;
--brand-700: #3a3ab8;
--brand-50: #eef0ff;
--accent: #00b4a6;
--accent-soft: #d8f5f2;
--ink: #101322;
--ink-2: #3a4060;
--muted: #6c7393;
--bg: #f6f7fb;
--white: #ffffff;
--surface: #ffffff;
--line: rgba(16, 19, 34, 0.1);
--line-2: rgba(16, 19, 34, 0.16);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--sh-1: 0 1px 2px rgba(16, 19, 34, 0.08);
--sh-2: 0 8px 24px rgba(16, 19, 34, 0.08);
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.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;
}
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 4px;
}
.page {
max-width: 1100px;
margin: 0 auto;
padding: clamp(28px, 5vw, 64px) clamp(16px, 4vw, 40px) 72px;
}
/* ---------- Masthead ---------- */
.masthead {
text-align: center;
margin-bottom: 36px;
}
.eyebrow {
margin: 0 0 12px;
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--brand);
}
.masthead h1 {
margin: 0 auto 14px;
max-width: 16ch;
font-size: clamp(1.7rem, 4vw, 2.6rem);
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.12;
}
.lede {
margin: 0 auto;
max-width: 56ch;
color: var(--ink-2);
font-size: clamp(0.96rem, 1.4vw, 1.08rem);
}
/* ---------- Controls ---------- */
.controls {
margin-top: 26px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
gap: 14px 22px;
}
.billing {
display: inline-flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
box-shadow: var(--sh-1);
}
.billing-text {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 0.9rem;
font-weight: 600;
color: var(--muted);
transition: color 0.18s ease;
}
.billing-text.active {
color: var(--ink);
}
.save-pill {
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.02em;
color: var(--accent);
background: var(--accent-soft);
padding: 2px 8px;
border-radius: 999px;
}
.switch {
position: relative;
width: 46px;
height: 26px;
padding: 0;
border: none;
border-radius: 999px;
background: var(--line-2);
cursor: pointer;
transition: background 0.2s ease;
}
.switch[aria-checked="true"] {
background: var(--brand);
}
.switch-knob {
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
background: var(--white);
border-radius: 50%;
box-shadow: var(--sh-1);
transition: transform 0.22s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.switch[aria-checked="true"] .switch-knob {
transform: translateX(20px);
}
.ghost-btn {
font-family: inherit;
font-size: 0.86rem;
font-weight: 600;
color: var(--brand-d);
background: transparent;
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 9px 16px;
cursor: pointer;
transition: background 0.16s ease, border-color 0.16s ease;
}
.ghost-btn:hover {
background: var(--brand-50);
border-color: var(--brand);
}
/* ---------- Table shell ---------- */
.table-scroll {
overflow-x: auto;
border-radius: var(--r-lg);
background: var(--white);
border: 1px solid var(--line);
box-shadow: var(--sh-2);
scroll-behavior: smooth;
}
.table-scroll:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
.matrix {
width: 100%;
min-width: 760px;
border-collapse: collapse;
text-align: center;
}
/* ---------- Sticky header (plan cards) ---------- */
.plan-row th {
position: sticky;
top: 0;
z-index: 5;
background: var(--white);
border-bottom: 1px solid var(--line);
vertical-align: top;
padding: 18px 14px 20px;
}
.plan-row .corner {
z-index: 7;
}
/* First column sticky on horizontal scroll */
.corner,
.matrix tbody th[scope="row"],
.cta-row .corner {
position: sticky;
left: 0;
z-index: 3;
background: var(--white);
text-align: left;
}
.plan-row .corner {
width: 210px;
}
.corner-title {
display: block;
font-size: 1.05rem;
font-weight: 800;
color: var(--ink);
}
.corner-sub {
display: block;
margin-top: 4px;
font-size: 0.8rem;
font-weight: 500;
color: var(--muted);
}
/* ---------- Plan card ---------- */
.plan {
width: 200px;
}
.plan-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.popular .plan-card {
padding-top: 10px;
}
.badge {
position: absolute;
top: -8px;
left: 50%;
transform: translateX(-50%);
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--white);
background: linear-gradient(135deg, var(--brand), var(--brand-d));
padding: 3px 12px;
border-radius: 999px;
white-space: nowrap;
box-shadow: var(--sh-1);
}
.plan-name {
margin: 0;
font-size: 1.12rem;
font-weight: 800;
letter-spacing: -0.01em;
}
.plan-blurb {
margin: 0;
font-size: 0.78rem;
color: var(--muted);
}
.plan-price {
margin: 8px 0 0;
display: flex;
align-items: baseline;
gap: 2px;
}
.amount {
font-size: 1.9rem;
font-weight: 800;
letter-spacing: -0.03em;
font-variant-numeric: tabular-nums;
transition: opacity 0.18s ease, transform 0.18s ease;
}
.amount.custom {
font-size: 1.5rem;
}
.amount.bump {
animation: bump 0.32s ease;
}
@keyframes bump {
0% { transform: translateY(0); opacity: 0.4; }
45% { transform: translateY(-5px); }
100% { transform: translateY(0); opacity: 1; }
}
.per {
font-size: 0.82rem;
font-weight: 600;
color: var(--muted);
}
.plan-note {
margin: 2px 0 0;
min-height: 1.1em;
font-size: 0.72rem;
color: var(--muted);
}
/* ---------- CTAs ---------- */
.cta {
font-family: inherit;
font-size: 0.86rem;
font-weight: 700;
border-radius: var(--r-sm);
padding: 9px 16px;
margin-top: 12px;
width: 100%;
cursor: pointer;
border: 1px solid transparent;
transition: transform 0.12s ease, background 0.16s ease,
border-color 0.16s ease, box-shadow 0.16s ease;
}
.cta:active {
transform: translateY(1px);
}
.cta-solid {
color: var(--white);
background: linear-gradient(135deg, var(--brand), var(--brand-d));
box-shadow: 0 6px 16px rgba(91, 91, 240, 0.32);
}
.cta-solid:hover {
box-shadow: 0 8px 20px rgba(91, 91, 240, 0.42);
}
.cta-soft {
color: var(--brand-d);
background: var(--brand-50);
border-color: transparent;
}
.cta-soft:hover {
background: #e3e6ff;
border-color: var(--line-2);
}
.hl-btn {
font-family: inherit;
font-size: 0.74rem;
font-weight: 600;
color: var(--muted);
background: transparent;
border: none;
margin-top: 8px;
padding: 3px 8px;
border-radius: 999px;
cursor: pointer;
transition: color 0.16s ease, background 0.16s ease;
}
.hl-btn::before {
content: "";
display: inline-block;
width: 8px;
height: 8px;
margin-right: 6px;
border-radius: 50%;
border: 1.5px solid currentColor;
vertical-align: middle;
}
.hl-btn:hover {
color: var(--brand-d);
background: var(--brand-50);
}
.hl-btn[aria-pressed="true"] {
color: var(--brand-d);
background: var(--brand-50);
}
.hl-btn[aria-pressed="true"]::before {
background: var(--brand);
border-color: var(--brand);
}
/* ---------- Group header rows ---------- */
.group-row th {
padding: 0;
background: #fafbff;
border-top: 1px solid var(--line);
border-bottom: 1px solid var(--line);
position: sticky;
left: 0;
}
.group-toggle {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
font-family: inherit;
font-size: 0.82rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--ink-2);
background: transparent;
border: none;
padding: 13px 18px;
cursor: pointer;
text-align: left;
}
.group-toggle:hover {
color: var(--brand-d);
}
.group-toggle .chev {
transition: transform 0.22s ease;
color: var(--muted);
}
.group-toggle[aria-expanded="false"] .chev {
transform: rotate(-90deg);
}
.group-icon {
display: inline-flex;
color: var(--brand);
}
.group-count {
margin-left: auto;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0;
text-transform: none;
color: var(--muted);
}
/* ---------- Feature rows ---------- */
.group-body tr {
transition: background 0.12s ease;
}
.group-body tr:hover {
background: #fafbff;
}
.group-body th[scope="row"] {
padding: 13px 18px;
font-size: 0.9rem;
font-weight: 600;
color: var(--ink);
border-top: 1px solid var(--line);
}
.group-body tr:hover th[scope="row"],
.group-body tr:hover .corner {
background: #fafbff;
}
.group-body td {
padding: 13px 14px;
font-size: 0.9rem;
font-weight: 600;
color: var(--ink-2);
border-top: 1px solid var(--line);
font-variant-numeric: tabular-nums;
}
.group-body.collapsed {
display: none;
}
/* check / x marks */
td[data-bool="true"]::after {
content: "";
display: inline-block;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--accent-soft)
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path d='M5 10.5l3 3 7-7' fill='none' stroke='%2300b4a6' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/></svg>")
center / 14px no-repeat;
vertical-align: middle;
}
td[data-bool="false"]::after {
content: "";
display: inline-block;
width: 18px;
height: 18px;
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path d='M6 6l8 8M14 6l-8 8' fill='none' stroke='%23b9bed4' stroke-width='2' stroke-linecap='round'/></svg>")
center / contain no-repeat;
vertical-align: middle;
}
/* hint tooltip */
.hint {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-left: 6px;
font-size: 0.66rem;
font-weight: 700;
font-style: normal;
color: var(--muted);
background: var(--bg);
border: 1px solid var(--line-2);
border-radius: 50%;
cursor: help;
position: relative;
vertical-align: middle;
}
.hint::after {
content: attr(data-tip);
position: absolute;
bottom: calc(100% + 8px);
left: 50%;
transform: translateX(-50%) translateY(4px);
width: max-content;
max-width: 220px;
padding: 8px 11px;
font-size: 0.74rem;
font-weight: 500;
line-height: 1.35;
text-transform: none;
letter-spacing: 0;
color: var(--white);
background: var(--ink);
border-radius: var(--r-sm);
box-shadow: var(--sh-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.16s ease, transform 0.16s ease;
z-index: 20;
}
.hint:hover::after,
.hint:focus-visible::after {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ---------- Footer CTA row ---------- */
.cta-row td {
padding: 18px 14px;
border-top: 1px solid var(--line);
}
.cta-foot {
margin-top: 0;
}
/* ---------- Highlight interaction ---------- */
.matrix.has-highlight td:not(.is-on):not(.corner),
.matrix.has-highlight .plan-row th:not(.is-on):not(.corner) {
opacity: 0.42;
filter: grayscale(0.25);
}
.matrix.has-highlight .plan-row th.is-on {
background: var(--brand-50);
}
.matrix.has-highlight .plan-row th.is-on::after {
content: "";
position: absolute;
inset: 0 0 -1px;
border: 2px solid var(--brand);
border-bottom: none;
border-radius: var(--r-md) var(--r-md) 0 0;
pointer-events: none;
}
.plan-row th {
transition: opacity 0.2s ease, filter 0.2s ease, background 0.2s ease;
}
.group-body td {
transition: opacity 0.2s ease, filter 0.2s ease, background 0.12s ease;
}
.group-body td.is-on,
.cta-row td.is-on {
background: rgba(91, 91, 240, 0.06);
}
/* ---------- Footnote ---------- */
.footnote {
margin: 18px 4px 0;
font-size: 0.78rem;
color: var(--muted);
text-align: center;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 28px;
transform: translateX(-50%) translateY(20px);
background: var(--ink);
color: var(--white);
font-size: 0.88rem;
font-weight: 600;
padding: 12px 20px;
border-radius: 999px;
box-shadow: var(--sh-2);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 50;
}
.toast.show {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.page {
padding: 24px 12px 60px;
}
.controls {
gap: 12px;
}
.matrix {
min-width: 660px;
}
.plan-row .corner {
width: 130px;
}
.corner-title {
font-size: 0.92rem;
}
.plan {
width: 158px;
}
.plan-row th {
padding: 14px 8px 16px;
}
.amount {
font-size: 1.55rem;
}
.group-body th[scope="row"],
.group-toggle {
padding-left: 12px;
padding-right: 10px;
}
.group-body td {
padding: 12px 8px;
font-size: 0.84rem;
}
.hint::after {
max-width: 160px;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
var PLAN_LABELS = {
starter: "Starter",
pro: "Pro",
scale: "Scale",
enterprise: "Enterprise",
};
/* ---------- Toast helper ---------- */
var toastEl = document.getElementById("toast");
var toastTimer = null;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("show");
window.clearTimeout(toastTimer);
toastTimer = window.setTimeout(function () {
toastEl.classList.remove("show");
}, 2400);
}
/* ---------- Billing toggle (monthly / annual) ---------- */
var matrix = document.getElementById("matrix");
var billingToggle = document.getElementById("billing-toggle");
var periodWord = document.getElementById("period-word");
var billingTexts = document.querySelectorAll(".billing-text");
var amounts = document.querySelectorAll(".amount[data-monthly]");
var notes = document.querySelectorAll(".plan-note[data-monthly-note]");
var annual = false;
function setBillingTextState() {
billingTexts.forEach(function (el, i) {
// index 0 = Monthly label, index 1 = Annual label
var isActive = i === 0 ? !annual : annual;
el.classList.toggle("active", isActive);
});
}
function renderPrices() {
amounts.forEach(function (el) {
var val = annual ? el.getAttribute("data-annual") : el.getAttribute("data-monthly");
el.textContent = "$" + val;
el.classList.remove("bump");
// force reflow so the animation re-triggers
void el.offsetWidth;
el.classList.add("bump");
});
notes.forEach(function (el) {
el.textContent = annual
? el.getAttribute("data-annual-note")
: el.getAttribute("data-monthly-note");
});
if (periodWord) periodWord.textContent = annual ? "annually" : "monthly";
}
function toggleBilling() {
annual = !annual;
billingToggle.setAttribute("aria-checked", annual ? "true" : "false");
setBillingTextState();
renderPrices();
toast(annual ? "Annual billing — save 20%" : "Switched to monthly billing");
}
if (billingToggle) {
billingToggle.addEventListener("click", toggleBilling);
setBillingTextState();
}
/* ---------- Collapsible feature groups ---------- */
var toggles = document.querySelectorAll(".group-toggle");
toggles.forEach(function (btn) {
btn.addEventListener("click", function () {
var bodyId = btn.getAttribute("aria-controls");
var body = document.getElementById(bodyId);
if (!body) return;
var expanded = btn.getAttribute("aria-expanded") === "true";
btn.setAttribute("aria-expanded", expanded ? "false" : "true");
body.classList.toggle("collapsed", expanded);
});
});
/* ---------- Highlight a plan (dim the others) ---------- */
var hlButtons = document.querySelectorAll(".hl-btn");
var resetBtn = document.getElementById("reset-highlight");
var activePlan = null;
function applyHighlight(plan) {
var cells = matrix.querySelectorAll("[data-plan]");
cells.forEach(function (cell) {
cell.classList.toggle("is-on", cell.getAttribute("data-plan") === plan);
});
matrix.classList.toggle("has-highlight", !!plan);
hlButtons.forEach(function (b) {
b.setAttribute("aria-pressed", b.getAttribute("data-plan") === plan ? "true" : "false");
});
if (resetBtn) resetBtn.hidden = !plan;
}
function setHighlight(plan) {
if (activePlan === plan) {
// toggling the same plan off
activePlan = null;
applyHighlight(null);
toast("Highlight cleared");
return;
}
activePlan = plan;
applyHighlight(plan);
toast("Showing " + (PLAN_LABELS[plan] || plan) + " column");
}
hlButtons.forEach(function (btn) {
btn.addEventListener("click", function () {
setHighlight(btn.getAttribute("data-plan"));
});
});
if (resetBtn) {
resetBtn.addEventListener("click", function () {
activePlan = null;
applyHighlight(null);
toast("Highlight cleared");
});
}
/* ---------- CTA buttons ---------- */
var ctas = document.querySelectorAll(".cta");
ctas.forEach(function (btn) {
btn.addEventListener("click", function () {
var name = btn.getAttribute("data-plan-name") || "this plan";
if (name === "Enterprise") {
toast("Opening sales contact for Enterprise…");
} else if (name === "Starter") {
toast("Creating your free Starter workspace…");
} else {
toast("Starting your 14-day " + name + " trial…");
}
});
});
/* ---------- Esc clears the highlight ---------- */
document.addEventListener("keydown", function (e) {
if (e.key === "Escape" && activePlan) {
activePlan = null;
applyHighlight(null);
toast("Highlight cleared");
}
});
// initial render
renderPrices();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Pricing — Feature comparison matrix</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="masthead">
<p class="eyebrow">Northwind · Pricing</p>
<h1>Compare every plan, feature by feature</h1>
<p class="lede">
From solo builders to enterprise platform teams — find the plan that
fits. Toggle billing, highlight the tier you care about, and collapse
the sections you don't.
</p>
<div class="controls">
<div class="billing" role="group" aria-label="Billing period">
<span id="billing-label" class="billing-text">Monthly</span>
<button
id="billing-toggle"
class="switch"
role="switch"
aria-checked="false"
aria-labelledby="billing-label"
>
<span class="switch-knob" aria-hidden="true"></span>
</button>
<span class="billing-text">
Annual
<span class="save-pill">Save 20%</span>
</span>
</div>
<button id="reset-highlight" class="ghost-btn" type="button" hidden>
Clear highlight
</button>
</div>
</header>
<main class="layout">
<div class="table-scroll" role="region" aria-label="Plan feature comparison" tabindex="0">
<table class="matrix" id="matrix">
<caption class="sr-only">
Northwind plans compared across core, collaboration, security and
support features.
</caption>
<thead>
<tr class="plan-row">
<th scope="col" class="corner">
<span class="corner-title">Plans</span>
<span class="corner-sub">Billed <span id="period-word">monthly</span></span>
</th>
<th scope="col" class="plan" data-plan="starter">
<div class="plan-card">
<p class="plan-name">Starter</p>
<p class="plan-blurb">For side projects & trials</p>
<p class="plan-price">
<span class="amount" data-monthly="0" data-annual="0">$0</span>
<span class="per">/mo</span>
</p>
<p class="plan-note">Free forever</p>
<button class="cta cta-soft" type="button" data-plan-name="Starter">
Get started
</button>
<button class="hl-btn" type="button" data-plan="starter" aria-pressed="false">
Highlight
</button>
</div>
</th>
<th scope="col" class="plan popular" data-plan="pro">
<div class="plan-card">
<span class="badge">Most popular</span>
<p class="plan-name">Pro</p>
<p class="plan-blurb">For growing teams</p>
<p class="plan-price">
<span class="amount" data-monthly="29" data-annual="23">$29</span>
<span class="per">/mo</span>
</p>
<p class="plan-note" data-monthly-note="per seat, billed monthly" data-annual-note="per seat, billed yearly">
per seat, billed monthly
</p>
<button class="cta cta-solid" type="button" data-plan-name="Pro">
Start free trial
</button>
<button class="hl-btn" type="button" data-plan="pro" aria-pressed="false">
Highlight
</button>
</div>
</th>
<th scope="col" class="plan" data-plan="scale">
<div class="plan-card">
<p class="plan-name">Scale</p>
<p class="plan-blurb">For scaling orgs</p>
<p class="plan-price">
<span class="amount" data-monthly="79" data-annual="63">$79</span>
<span class="per">/mo</span>
</p>
<p class="plan-note" data-monthly-note="per seat, billed monthly" data-annual-note="per seat, billed yearly">
per seat, billed monthly
</p>
<button class="cta cta-soft" type="button" data-plan-name="Scale">
Start free trial
</button>
<button class="hl-btn" type="button" data-plan="scale" aria-pressed="false">
Highlight
</button>
</div>
</th>
<th scope="col" class="plan" data-plan="enterprise">
<div class="plan-card">
<p class="plan-name">Enterprise</p>
<p class="plan-blurb">For platform teams</p>
<p class="plan-price">
<span class="amount custom">Custom</span>
</p>
<p class="plan-note">Volume pricing</p>
<button class="cta cta-soft" type="button" data-plan-name="Enterprise">
Contact sales
</button>
<button class="hl-btn" type="button" data-plan="enterprise" aria-pressed="false">
Highlight
</button>
</div>
</th>
</tr>
</thead>
<!-- ===== CORE ===== -->
<tbody class="group" data-group="core">
<tr class="group-row">
<th scope="colgroup" colspan="5">
<button class="group-toggle" type="button" aria-expanded="true" aria-controls="grp-core">
<svg class="chev" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="group-icon" aria-hidden="true">
<svg viewBox="0 0 20 20" width="16" height="16"><path d="M3 4.5A1.5 1.5 0 014.5 3h11A1.5 1.5 0 0117 4.5v11a1.5 1.5 0 01-1.5 1.5h-11A1.5 1.5 0 013 15.5zM7 7v6M13 7v6" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
</span>
Core platform
<span class="group-count">4 features</span>
</button>
</th>
</tr>
</tbody>
<tbody class="group-body" id="grp-core">
<tr>
<th scope="row">
Projects
<span class="hint" data-tip="A workspace for tasks, docs and automations.">i</span>
</th>
<td data-plan="starter">3</td>
<td data-plan="pro">25</td>
<td data-plan="scale">Unlimited</td>
<td data-plan="enterprise">Unlimited</td>
</tr>
<tr>
<th scope="row">Storage</th>
<td data-plan="starter">2 GB</td>
<td data-plan="pro">100 GB</td>
<td data-plan="scale">1 TB</td>
<td data-plan="enterprise">Unlimited</td>
</tr>
<tr>
<th scope="row">API calls / mo</th>
<td data-plan="starter">5,000</td>
<td data-plan="pro">250,000</td>
<td data-plan="scale">5M</td>
<td data-plan="enterprise">Custom</td>
</tr>
<tr>
<th scope="row">Custom domains</th>
<td data-plan="starter" data-bool="false"></td>
<td data-plan="pro">1</td>
<td data-plan="scale">10</td>
<td data-plan="enterprise">Unlimited</td>
</tr>
</tbody>
<!-- ===== COLLABORATION ===== -->
<tbody class="group" data-group="collab">
<tr class="group-row">
<th scope="colgroup" colspan="5">
<button class="group-toggle" type="button" aria-expanded="true" aria-controls="grp-collab">
<svg class="chev" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="group-icon" aria-hidden="true">
<svg viewBox="0 0 20 20" width="16" height="16"><circle cx="7" cy="7" r="2.6" fill="none" stroke="currentColor" stroke-width="1.6"/><circle cx="14" cy="8" r="2.1" fill="none" stroke="currentColor" stroke-width="1.6"/><path d="M2.5 16c0-2.5 2-4 4.5-4s4.5 1.5 4.5 4M12 16c0-1.9 1.3-3.2 3-3.2s2.5 1 2.7 2.4" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
</span>
Collaboration
<span class="group-count">4 features</span>
</button>
</th>
</tr>
</tbody>
<tbody class="group-body" id="grp-collab">
<tr>
<th scope="row">Team members</th>
<td data-plan="starter">1</td>
<td data-plan="pro">10</td>
<td data-plan="scale">50</td>
<td data-plan="enterprise">Unlimited</td>
</tr>
<tr>
<th scope="row">Guest collaborators</th>
<td data-plan="starter" data-bool="false"></td>
<td data-plan="pro">5</td>
<td data-plan="scale">Unlimited</td>
<td data-plan="enterprise">Unlimited</td>
</tr>
<tr>
<th scope="row">Real-time editing</th>
<td data-plan="starter" data-bool="false"></td>
<td data-plan="pro" data-bool="true"></td>
<td data-plan="scale" data-bool="true"></td>
<td data-plan="enterprise" data-bool="true"></td>
</tr>
<tr>
<th scope="row">
Activity history
<span class="hint" data-tip="How far back you can scroll the audit feed.">i</span>
</th>
<td data-plan="starter">7 days</td>
<td data-plan="pro">90 days</td>
<td data-plan="scale">1 year</td>
<td data-plan="enterprise">Unlimited</td>
</tr>
</tbody>
<!-- ===== SECURITY ===== -->
<tbody class="group" data-group="security">
<tr class="group-row">
<th scope="colgroup" colspan="5">
<button class="group-toggle" type="button" aria-expanded="true" aria-controls="grp-security">
<svg class="chev" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="group-icon" aria-hidden="true">
<svg viewBox="0 0 20 20" width="16" height="16"><path d="M10 2.5l6 2.2v4.6c0 4-2.6 6.6-6 8-3.4-1.4-6-4-6-8V4.7z" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/><path d="M7.3 10l1.8 1.8L13 8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
Security & compliance
<span class="group-count">4 features</span>
</button>
</th>
</tr>
</tbody>
<tbody class="group-body" id="grp-security">
<tr>
<th scope="row">SSO / SAML</th>
<td data-plan="starter" data-bool="false"></td>
<td data-plan="pro" data-bool="false"></td>
<td data-plan="scale" data-bool="true"></td>
<td data-plan="enterprise" data-bool="true"></td>
</tr>
<tr>
<th scope="row">Role-based access</th>
<td data-plan="starter" data-bool="false"></td>
<td data-plan="pro" data-bool="true"></td>
<td data-plan="scale" data-bool="true"></td>
<td data-plan="enterprise" data-bool="true"></td>
</tr>
<tr>
<th scope="row">
Audit logs
<span class="hint" data-tip="Exportable, tamper-evident event logs.">i</span>
</th>
<td data-plan="starter" data-bool="false"></td>
<td data-plan="pro" data-bool="false"></td>
<td data-plan="scale">90 days</td>
<td data-plan="enterprise">Unlimited</td>
</tr>
<tr>
<th scope="row">Compliance</th>
<td data-plan="starter" data-bool="false"></td>
<td data-plan="pro">SOC 2</td>
<td data-plan="scale">SOC 2 + HIPAA</td>
<td data-plan="enterprise">Custom DPA</td>
</tr>
</tbody>
<!-- ===== SUPPORT ===== -->
<tbody class="group" data-group="support">
<tr class="group-row">
<th scope="colgroup" colspan="5">
<button class="group-toggle" type="button" aria-expanded="true" aria-controls="grp-support">
<svg class="chev" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<span class="group-icon" aria-hidden="true">
<svg viewBox="0 0 20 20" width="16" height="16"><path d="M3 10a7 7 0 1114 0v3.5A2.5 2.5 0 0111.5 16H10" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><rect x="2.4" y="9.4" width="3" height="4.6" rx="1.3" fill="none" stroke="currentColor" stroke-width="1.6"/><rect x="14.6" y="9.4" width="3" height="4.6" rx="1.3" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
</span>
Support
<span class="group-count">3 features</span>
</button>
</th>
</tr>
</tbody>
<tbody class="group-body" id="grp-support">
<tr>
<th scope="row">Support channel</th>
<td data-plan="starter">Community</td>
<td data-plan="pro">Email</td>
<td data-plan="scale">Priority email</td>
<td data-plan="enterprise">Dedicated</td>
</tr>
<tr>
<th scope="row">Response SLA</th>
<td data-plan="starter" data-bool="false"></td>
<td data-plan="pro">48 h</td>
<td data-plan="scale">8 h</td>
<td data-plan="enterprise">1 h</td>
</tr>
<tr>
<th scope="row">Onboarding manager</th>
<td data-plan="starter" data-bool="false"></td>
<td data-plan="pro" data-bool="false"></td>
<td data-plan="scale" data-bool="true"></td>
<td data-plan="enterprise" data-bool="true"></td>
</tr>
</tbody>
<tfoot>
<tr class="cta-row">
<td class="corner"></td>
<td data-plan="starter"><button class="cta cta-soft cta-foot" type="button" data-plan-name="Starter">Get started</button></td>
<td data-plan="pro"><button class="cta cta-solid cta-foot" type="button" data-plan-name="Pro">Start free trial</button></td>
<td data-plan="scale"><button class="cta cta-soft cta-foot" type="button" data-plan-name="Scale">Start free trial</button></td>
<td data-plan="enterprise"><button class="cta cta-soft cta-foot" type="button" data-plan-name="Enterprise">Contact sales</button></td>
</tr>
</tfoot>
</table>
</div>
<p class="footnote">
Prices in USD, excluding tax. Annual plans are billed yearly at the
discounted rate. Illustrative pricing for a fictional product.
</p>
</main>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Feature comparison matrix
A complete pricing comparison table for the fictional Northwind platform. Four plan columns — Starter, Pro, Scale and Enterprise — run across the top inside sticky header cards that carry the plan name, a one-line blurb, the live price, a contextual CTA and a Highlight control. The body is split into four collapsible sections (Core platform, Collaboration, Security & compliance, and Support) totalling fifteen features. Each cell either resolves a boolean to an inline check or cross SVG, or shows a concrete value such as 100 GB, 48 h or Unlimited. A Most popular badge calls out the Pro tier.
The matrix is built for scanning and deciding. A monthly/annual switch retunes every numeric price with a subtle bump animation and updates the per-seat billing notes and the header subtitle. Pressing Highlight on any plan dims and desaturates the other columns so a single tier stands out top to bottom; pressing it again, hitting the Clear highlight button, or tapping Esc restores the full view. Every feature group collapses from its sticky section header, and the CTA buttons fire a toast confirming the trial, signup or sales flow.
On narrow screens the table scrolls horizontally inside a focusable region while the feature-name column stays pinned to the left edge, so the row labels never scroll out of reach. The whole component is keyboard-usable with visible focus rings, the switch is exposed as an ARIA switch, group toggles report their expanded state, and the toast announces through an aria-live region. The layout, marks and tooltips remain legible down to 360px.
Illustrative UI only — fictional brand, plans and pricing.