SaaS — Upgrade / Plan-compare Modal
A centered upgrade dialog that compares the current plan against higher tiers with feature deltas highlighted as gains, a monthly/annual billing toggle, a seat stepper, and a live order summary with a proration note. Opening from a CTA traps focus and supports Esc to dismiss; billing and seat changes recompute totals in real time, and confirming reveals a polished success state listing exactly what the team just unlocked.
MCP
Code
:root {
--bg: #f7f8fb;
--surface: #ffffff;
--surface-2: #fbfbfe;
--ink: #0f1222;
--muted: #646b85;
--brand: #6366f1;
--brand-d: #4f46e5;
--brand-soft: #eef0ff;
--ok: #16a34a;
--warn: #d97706;
--danger: #dc2626;
--line: rgba(15, 18, 34, .1);
--line-2: rgba(15, 18, 34, .06);
--shadow-sm: 0 1px 2px rgba(15, 18, 34, .06);
--shadow-md: 0 6px 24px rgba(15, 18, 34, .08);
--shadow-lg: 0 24px 64px rgba(15, 18, 34, .22);
--radius: 14px;
}
* { box-sizing: border-box; }
html, body { margin: 0; }
body {
font-family: "Inter", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
background:
radial-gradient(900px 500px at 88% -8%, #eef0ff 0%, transparent 60%),
radial-gradient(700px 420px at -10% 110%, #f0f7ff 0%, transparent 55%),
var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
/* ---------- Page shell ---------- */
.page { max-width: 960px; margin: 0 auto; padding: 22px 20px 60px; }
.topbar {
display: flex; align-items: center; justify-content: space-between;
gap: 16px; flex-wrap: wrap;
padding: 12px 16px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
box-shadow: var(--shadow-sm);
}
.brand { display: flex; align-items: center; gap: 10px; }
.logo {
display: grid; place-items: center;
width: 34px; height: 34px; border-radius: 9px;
color: #fff; background: linear-gradient(135deg, var(--brand), var(--brand-d));
}
.brand-name { font-weight: 800; letter-spacing: -.01em; }
.plan-pill {
font-size: .74rem; font-weight: 600; color: var(--muted);
background: var(--surface-2); border: 1px solid var(--line);
padding: 3px 9px; border-radius: 999px;
}
.topnav { display: flex; gap: 18px; }
.usage { display: flex; flex-direction: column; align-items: flex-end; }
.usage-label { font-size: .7rem; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
.usage strong { font-size: .95rem; }
.hero { margin-top: 40px; display: grid; place-items: center; }
.hero-card {
max-width: 620px; text-align: center;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 20px;
padding: 40px 34px;
box-shadow: var(--shadow-md);
}
.hero-badge {
display: inline-block;
font-size: .76rem; font-weight: 600; color: var(--warn);
background: #fff7ed; border: 1px solid #fed7aa;
padding: 5px 12px; border-radius: 999px; margin-bottom: 16px;
}
.hero-card h1 { margin: 0 0 10px; font-size: clamp(1.5rem, 4vw, 2.1rem); letter-spacing: -.02em; }
.hero-card h1 em { font-style: normal; color: var(--brand-d); }
.hero-sub { margin: 0 auto 22px; color: var(--muted); max-width: 460px; }
.hero-note { margin: 14px 0 0; font-size: .8rem; color: var(--muted); }
/* ---------- Buttons ---------- */
.btn {
font: inherit; font-weight: 600;
border-radius: 10px; border: 1px solid transparent;
padding: 10px 16px; cursor: pointer;
transition: transform .08s ease, background .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.btn:active { transform: translateY(1px); }
.btn-lg { padding: 13px 24px; font-size: 1.02rem; }
.btn-primary { background: var(--brand-d); color: #fff; box-shadow: 0 6px 16px rgba(79, 70, 229, .28); }
.btn-primary:hover { background: #4338ca; }
.btn-outline { background: var(--surface); color: var(--brand-d); border-color: var(--brand); }
.btn-outline:hover { background: var(--brand-soft); }
.btn-ghost { background: transparent; color: var(--muted); border-color: var(--line); }
.btn-ghost:hover { background: var(--surface-2); color: var(--ink); }
.btn-ghost:disabled { opacity: .55; cursor: default; }
:focus-visible { outline: 3px solid rgba(99, 102, 241, .45); outline-offset: 2px; border-radius: 8px; }
/* ---------- Overlay + modal ---------- */
.overlay {
position: fixed; inset: 0;
background: rgba(15, 18, 34, .5);
backdrop-filter: blur(3px);
z-index: 40;
animation: fade .2s ease;
}
.modal {
position: fixed; inset: 0; z-index: 50;
display: grid; place-items: center;
padding: 18px;
}
.modal-panel {
width: 100%; max-width: 880px;
max-height: 92vh; overflow: auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 18px;
box-shadow: var(--shadow-lg);
animation: pop .22s cubic-bezier(.2, .9, .3, 1.1);
}
@keyframes fade { from { opacity: 0; } }
@keyframes pop { from { opacity: 0; transform: translateY(14px) scale(.98); } }
.modal-head {
display: flex; align-items: flex-start; justify-content: space-between;
gap: 16px; padding: 22px 24px 4px;
}
.modal-head h2 { margin: 0; font-size: 1.3rem; letter-spacing: -.01em; }
.modal-desc { margin: 6px 0 0; color: var(--muted); font-size: .9rem; max-width: 540px; }
.icon-btn {
flex: none; display: grid; place-items: center;
width: 36px; height: 36px; border-radius: 9px;
background: var(--surface-2); border: 1px solid var(--line);
color: var(--muted); cursor: pointer;
}
.icon-btn:hover { background: #f1f1f8; color: var(--ink); }
/* ---------- Controls ---------- */
.controls {
display: flex; align-items: center; justify-content: space-between;
gap: 16px; flex-wrap: wrap;
padding: 16px 24px;
}
.billing-toggle {
display: inline-flex; background: var(--surface-2);
border: 1px solid var(--line); border-radius: 11px; padding: 4px;
}
.seg {
font: inherit; font-weight: 600; font-size: .9rem;
border: 0; background: transparent; color: var(--muted);
padding: 8px 14px; border-radius: 8px; cursor: pointer;
display: inline-flex; align-items: center; gap: 7px;
transition: background .15s, color .15s, box-shadow .15s;
}
.seg.active { background: var(--surface); color: var(--ink); box-shadow: var(--shadow-sm); }
.save-tag {
font-size: .68rem; font-weight: 700; color: var(--ok);
background: #e9f7ef; border-radius: 999px; padding: 2px 7px;
}
.seat-selector { display: flex; align-items: center; gap: 10px; }
.seat-selector label { font-size: .85rem; font-weight: 600; color: var(--muted); }
.stepper {
display: inline-flex; align-items: center;
background: var(--surface); border: 1px solid var(--line); border-radius: 10px;
overflow: hidden;
}
.step-btn {
width: 38px; height: 40px; border: 0; background: var(--surface-2);
font-size: 1.2rem; font-weight: 600; color: var(--ink); cursor: pointer;
transition: background .12s;
}
.step-btn:hover { background: #ececf6; }
.step-btn:disabled { opacity: .4; cursor: default; }
.seat-input {
width: 50px; height: 40px; border: 0; text-align: center;
font: inherit; font-weight: 700; font-size: 1rem; color: var(--ink);
background: var(--surface);
}
.seat-input:focus { outline: none; }
/* ---------- Plans ---------- */
.plans {
display: grid; grid-template-columns: repeat(3, 1fr);
gap: 14px; padding: 6px 24px 20px;
}
.plan {
position: relative;
background: var(--surface); border: 1px solid var(--line);
border-radius: 14px; padding: 18px 16px;
display: flex; flex-direction: column; gap: 14px;
transition: border-color .15s, box-shadow .15s, transform .12s;
}
.plan[tabindex]:hover { transform: translateY(-2px); box-shadow: var(--shadow-md); }
.plan.current { background: var(--surface-2); }
.plan.recommended { border-color: var(--brand); box-shadow: 0 8px 28px rgba(99, 102, 241, .16); }
.plan[aria-pressed="true"]:not(.current) {
border-color: var(--brand-d);
box-shadow: 0 0 0 2px rgba(99, 102, 241, .35), var(--shadow-md);
}
.ribbon {
position: absolute; top: -11px; left: 50%; transform: translateX(-50%);
font-size: .68rem; font-weight: 700; letter-spacing: .03em; text-transform: uppercase;
color: #fff; background: linear-gradient(135deg, var(--brand), var(--brand-d));
padding: 4px 11px; border-radius: 999px; white-space: nowrap;
}
.plan-top { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.plan-top h3 { margin: 0; font-size: 1.05rem; display: flex; align-items: center; gap: 7px; }
.tag-current {
font-size: .62rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
color: var(--muted); background: var(--bg); border: 1px solid var(--line);
padding: 2px 6px; border-radius: 999px;
}
.price { white-space: nowrap; }
.price .amount { font-size: 1.35rem; font-weight: 800; letter-spacing: -.02em; }
.price .per { font-size: .72rem; color: var(--muted); margin-left: 2px; }
.features { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.features li {
display: flex; align-items: center; gap: 9px;
font-size: .85rem; color: var(--ink);
}
.features li.gain { font-weight: 600; }
.ico {
flex: none; display: grid; place-items: center;
width: 18px; height: 18px; border-radius: 999px; font-size: .72rem; font-weight: 700;
}
.ico.ok { color: var(--ok); background: #e9f7ef; }
.ico.no { color: var(--muted); background: var(--bg); }
.gain-tag {
margin-left: auto; font-size: .62rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .03em; color: var(--brand-d); background: var(--brand-soft);
padding: 2px 6px; border-radius: 999px;
}
.plan .btn { margin-top: auto; }
/* ---------- Summary ---------- */
.summary {
display: flex; align-items: flex-end; justify-content: space-between;
gap: 20px; flex-wrap: wrap;
padding: 18px 24px 22px;
border-top: 1px solid var(--line);
background: var(--surface-2);
border-radius: 0 0 18px 18px;
}
.summary-lines { flex: 1 1 280px; }
.sum-row { display: flex; justify-content: space-between; gap: 16px; font-size: .9rem; padding: 3px 0; }
.sum-row.muted { color: var(--muted); }
.sum-row.muted span:last-child { color: var(--ok); }
.sum-row.total { font-weight: 800; font-size: 1.05rem; padding-top: 8px; margin-top: 4px; border-top: 1px dashed var(--line); }
.proration-note { margin: 10px 0 0; font-size: .76rem; color: var(--muted); }
.summary-actions { display: flex; gap: 10px; flex: none; }
/* ---------- Success view ---------- */
.view-success .success-inner {
text-align: center; padding: 46px 28px 40px; max-width: 480px; margin: 0 auto;
}
.success-check {
display: grid; place-items: center; margin: 0 auto 18px;
width: 64px; height: 64px; border-radius: 999px;
color: #fff; background: linear-gradient(135deg, #22c55e, var(--ok));
box-shadow: 0 10px 26px rgba(22, 163, 74, .35);
animation: pop .3s cubic-bezier(.2, .9, .3, 1.2);
}
.view-success h2 { margin: 0 0 6px; font-size: 1.5rem; letter-spacing: -.01em; }
.view-success h2:focus { outline: none; }
.success-sub { margin: 0 0 18px; color: var(--muted); }
.gain-list {
list-style: none; margin: 0 0 24px; padding: 0;
display: grid; gap: 8px; text-align: left;
}
.gain-list li {
position: relative; padding: 10px 12px 10px 38px;
background: var(--surface-2); border: 1px solid var(--line);
border-radius: 10px; font-size: .88rem; font-weight: 500;
}
.gain-list li::before {
content: "✓"; position: absolute; left: 12px; top: 50%; transform: translateY(-50%);
color: var(--ok); font-weight: 800;
}
.success-actions { display: flex; justify-content: center; }
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%);
display: flex; flex-direction: column; gap: 8px; z-index: 80; width: max-content; max-width: 92vw;
}
.toast {
background: var(--ink); color: #fff;
padding: 11px 16px; border-radius: 10px; font-size: .88rem; font-weight: 500;
box-shadow: var(--shadow-lg);
animation: toastIn .25s ease;
}
@keyframes toastIn { from { opacity: 0; transform: translateY(10px); } }
/* ---------- Responsive ---------- */
@media (max-width: 760px) {
.plans { grid-template-columns: 1fr; }
.plan.current { order: 3; }
.summary { flex-direction: column; align-items: stretch; }
.summary-actions { justify-content: stretch; }
.summary-actions .btn { flex: 1; }
}
@media (max-width: 440px) {
.controls { flex-direction: column; align-items: stretch; }
.billing-toggle, .seat-selector { justify-content: space-between; }
.modal-head h2 { font-size: 1.15rem; }
.topnav { gap: 14px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
}(function () {
"use strict";
// ---- State ----
var state = {
plan: "pro", // pro | business
period: "monthly", // monthly | annual
seats: 8,
};
var MIN_SEATS = 1;
var MAX_SEATS = 200;
var prices = {
pro: { monthly: 29, annual: 23, label: "Pro" },
business: { monthly: 49, annual: 39, label: "Business" },
};
// Fraction of the current cycle already elapsed (fictional) → proration credit on unused Starter time is $0,
// but we model a small credit from a previously paid add-on to make the note meaningful.
var REMAINING_CYCLE_FRACTION = 0.62; // ~19 days left of 30
var RENEW_DATE = "Jul 1";
// ---- Elements ----
var overlay = document.getElementById("overlay");
var modal = document.getElementById("upgradeModal");
var panel = document.getElementById("modalPanel");
var viewCompare = document.getElementById("viewCompare");
var viewSuccess = document.getElementById("viewSuccess");
var openBtn = document.getElementById("openUpgrade");
var closeBtn = document.getElementById("closeBtn");
var cancelBtn = document.getElementById("cancelBtn");
var confirmBtn = document.getElementById("confirmBtn");
var doneBtn = document.getElementById("doneBtn");
var billMonthly = document.getElementById("billMonthly");
var billAnnual = document.getElementById("billAnnual");
var seatInput = document.getElementById("seatInput");
var seatMinus = document.getElementById("seatMinus");
var seatPlus = document.getElementById("seatPlus");
var planCards = Array.prototype.slice.call(document.querySelectorAll(".plan[data-plan]"));
var selectBtns = Array.prototype.slice.call(document.querySelectorAll(".select-plan"));
var sumPlan = document.getElementById("sumPlan");
var sumSeats = document.getElementById("sumSeats");
var sumPeriod = document.getElementById("sumPeriod");
var sumBase = document.getElementById("sumBase");
var sumProration = document.getElementById("sumProration");
var sumTotal = document.getElementById("sumTotal");
var prorationRow = document.getElementById("prorationRow");
var lastFocused = null;
// ---- Helpers ----
function money(n) {
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function toast(msg) {
var wrap = document.getElementById("toastWrap");
var el = document.createElement("div");
el.className = "toast";
el.textContent = msg;
wrap.appendChild(el);
setTimeout(function () {
el.style.transition = "opacity .3s, transform .3s";
el.style.opacity = "0";
el.style.transform = "translateY(8px)";
setTimeout(function () { el.remove(); }, 320);
}, 2600);
}
// ---- Pricing render ----
function unit() {
return prices[state.plan][state.period];
}
function monthlyTotal() {
// unit is per-seat-per-month (annual already discounted per month)
return unit() * state.seats;
}
function renderPrices() {
// update per-card amounts to reflect the billing period
Object.keys(prices).forEach(function (key) {
var amt = document.querySelector('.amount[data-price="' + key + '"]');
if (amt) amt.textContent = "$" + prices[key][state.period];
});
}
function renderSummary() {
var base = monthlyTotal();
// Annual is billed up front for 12 months; monthly billed per month.
var billedNow = state.period === "annual" ? base * 12 : base;
// Proration credit: a small unused credit applied to the first invoice.
var credit = state.period === "annual" ? 0 : Math.min(base * (1 - REMAINING_CYCLE_FRACTION), base) * 0.25;
credit = Math.round(credit * 100) / 100;
var dueToday = Math.max(0, billedNow - credit);
sumPlan.textContent = prices[state.plan].label;
sumSeats.textContent = state.seats + (state.seats === 1 ? " seat" : " seats");
sumPeriod.textContent = state.period === "annual" ? "billed annually" : "monthly";
sumBase.textContent = money(billedNow);
if (credit > 0) {
prorationRow.style.display = "";
sumProration.textContent = "−" + money(credit);
} else {
prorationRow.style.display = "none";
}
sumTotal.textContent = money(dueToday);
var note = document.getElementById("prorationNote");
if (state.period === "annual") {
note.innerHTML =
'Billed annually today, then <span id="renewAmount">' +
money(base * 12) + "/yr</span> on " + RENEW_DATE + ".";
} else {
note.innerHTML =
"You'll be charged a prorated amount now, then " +
'<span id="renewAmount">' + money(base) + "/mo</span> on " + RENEW_DATE + ".";
}
}
function renderPlanSelection() {
planCards.forEach(function (card) {
var on = card.getAttribute("data-plan") === state.plan;
card.setAttribute("aria-pressed", on ? "true" : "false");
});
selectBtns.forEach(function (b) {
var on = b.getAttribute("data-plan") === state.plan;
b.textContent = on ? "✓ Selected" : "Select " + prices[b.getAttribute("data-plan")].label;
});
}
function renderAll() {
renderPrices();
renderPlanSelection();
renderSummary();
}
// ---- Billing toggle ----
function setPeriod(p) {
state.period = p;
var monthlyOn = p === "monthly";
billMonthly.classList.toggle("active", monthlyOn);
billAnnual.classList.toggle("active", !monthlyOn);
billMonthly.setAttribute("aria-pressed", String(monthlyOn));
billAnnual.setAttribute("aria-pressed", String(!monthlyOn));
renderAll();
}
billMonthly.addEventListener("click", function () { setPeriod("monthly"); });
billAnnual.addEventListener("click", function () {
setPeriod("annual");
toast("Annual billing — you save 20%.");
});
// ---- Seats ----
function setSeats(n) {
n = parseInt(n, 10);
if (isNaN(n)) n = MIN_SEATS;
n = Math.max(MIN_SEATS, Math.min(MAX_SEATS, n));
state.seats = n;
seatInput.value = n;
seatMinus.disabled = n <= MIN_SEATS;
seatPlus.disabled = n >= MAX_SEATS;
renderSummary();
}
seatMinus.addEventListener("click", function () { setSeats(state.seats - 1); });
seatPlus.addEventListener("click", function () { setSeats(state.seats + 1); });
seatInput.addEventListener("input", function () {
var cleaned = seatInput.value.replace(/[^0-9]/g, "");
seatInput.value = cleaned;
});
seatInput.addEventListener("change", function () { setSeats(seatInput.value); });
seatInput.addEventListener("blur", function () { setSeats(seatInput.value); });
// ---- Plan selection ----
function selectPlan(plan) {
if (!prices[plan]) return;
state.plan = plan;
renderAll();
}
planCards.forEach(function (card) {
card.addEventListener("click", function (e) {
if (e.target.closest("button")) return; // button handles itself
selectPlan(card.getAttribute("data-plan"));
});
card.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
selectPlan(card.getAttribute("data-plan"));
}
});
});
selectBtns.forEach(function (b) {
b.addEventListener("click", function () { selectPlan(b.getAttribute("data-plan")); });
});
// ---- Focus trap ----
function focusable() {
return Array.prototype.slice.call(
panel.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
)
).filter(function (el) {
return el.offsetParent !== null;
});
}
function trap(e) {
if (e.key === "Escape") {
e.preventDefault();
closeModal();
return;
}
if (e.key !== "Tab") return;
var nodes = focusable();
if (!nodes.length) return;
var first = nodes[0];
var last = nodes[nodes.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
// ---- Open / close ----
function openModal() {
lastFocused = document.activeElement;
// reset to compare view
viewSuccess.hidden = true;
viewCompare.hidden = false;
overlay.hidden = false;
modal.hidden = false;
document.body.style.overflow = "hidden";
renderAll();
setSeats(state.seats);
document.addEventListener("keydown", trap);
setTimeout(function () { closeBtn.focus(); }, 30);
}
function closeModal() {
overlay.hidden = true;
modal.hidden = true;
document.body.style.overflow = "";
document.removeEventListener("keydown", trap);
if (lastFocused && lastFocused.focus) lastFocused.focus();
}
openBtn.addEventListener("click", openModal);
closeBtn.addEventListener("click", closeModal);
cancelBtn.addEventListener("click", closeModal);
overlay.addEventListener("click", closeModal);
doneBtn.addEventListener("click", function () {
closeModal();
toast("Welcome to " + prices[state.plan].label + " — enjoy the new limits!");
});
// ---- Confirm → success ----
var GAINS = {
pro: [
"Unlimited seats — invite the whole team",
"Unlimited projects",
"Advanced analytics dashboards",
"Priority support & faster response SLAs",
],
business: [
"Everything in Pro, plus more",
"Dedicated customer success manager",
"SSO & SCIM provisioning",
"Audit log & advanced security controls",
],
};
confirmBtn.addEventListener("click", function () {
confirmBtn.disabled = true;
confirmBtn.textContent = "Processing…";
setTimeout(function () {
confirmBtn.disabled = false;
confirmBtn.textContent = "Confirm upgrade";
// populate success view
document.getElementById("successPlan").textContent = prices[state.plan].label;
document.getElementById("successSeats").textContent =
state.seats + (state.seats === 1 ? " seat" : " seats");
var list = document.getElementById("gainList");
list.innerHTML = "";
GAINS[state.plan].forEach(function (g) {
var li = document.createElement("li");
li.textContent = g;
list.appendChild(li);
});
// reflect on the page shell
document.getElementById("planPill").textContent = prices[state.plan].label + " plan";
document.getElementById("seatUsage").textContent = "5 / " + state.seats;
viewCompare.hidden = true;
viewSuccess.hidden = false;
panel.scrollTop = 0;
setTimeout(function () { document.getElementById("successTitle").focus(); }, 40);
}, 850);
});
// ---- Init ----
renderAll();
setSeats(state.seats);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Upgrade your plan — Northwind</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>
<main class="page" role="main">
<header class="topbar">
<div class="brand">
<span class="logo" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none"><path d="M4 14 12 4l8 10-8 6z" fill="currentColor"/></svg>
</span>
<span class="brand-name">Northwind</span>
<span class="plan-pill" id="planPill">Starter plan</span>
</div>
<nav class="topnav" aria-label="Account usage">
<div class="usage">
<span class="usage-label">Seats</span>
<strong id="seatUsage">5 / 5</strong>
</div>
<div class="usage">
<span class="usage-label">Projects</span>
<strong>3 / 3</strong>
</div>
</nav>
</header>
<section class="hero">
<div class="hero-card">
<div class="hero-badge">You're hitting your limits</div>
<h1>Your team has outgrown <em>Starter</em>.</h1>
<p class="hero-sub">All 5 seats and 3 projects are in use, and 2 invites are pending. Upgrade to keep your team moving without limits.</p>
<button class="btn btn-primary btn-lg" id="openUpgrade" type="button" aria-haspopup="dialog">
Compare plans & upgrade
</button>
<p class="hero-note">No charge today on annual preview · cancel anytime</p>
</div>
</section>
</main>
<!-- Upgrade modal -->
<div class="overlay" id="overlay" hidden></div>
<div class="modal" id="upgradeModal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" aria-describedby="modalDesc" hidden>
<div class="modal-panel" id="modalPanel">
<!-- COMPARE VIEW -->
<div class="view view-compare" id="viewCompare">
<header class="modal-head">
<div>
<h2 id="modalTitle">Upgrade your workspace</h2>
<p id="modalDesc" class="modal-desc">Compare your current plan with what you'll unlock. Everything in lower tiers carries over.</p>
</div>
<button class="icon-btn" id="closeBtn" type="button" aria-label="Close dialog">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6 6 18"/></svg>
</button>
</header>
<div class="controls">
<div class="billing-toggle" role="group" aria-label="Billing period">
<button class="seg active" id="billMonthly" type="button" aria-pressed="true">Monthly</button>
<button class="seg" id="billAnnual" type="button" aria-pressed="false">
Annual <span class="save-tag">save 20%</span>
</button>
</div>
<div class="seat-selector">
<label id="seatLabel">Seats</label>
<div class="stepper" role="group" aria-labelledby="seatLabel">
<button class="step-btn" id="seatMinus" type="button" aria-label="Remove a seat">−</button>
<input class="seat-input" id="seatInput" type="text" inputmode="numeric" value="8" aria-label="Number of seats" />
<button class="step-btn" id="seatPlus" type="button" aria-label="Add a seat">+</button>
</div>
</div>
</div>
<div class="plans" role="list">
<article class="plan current" role="listitem">
<div class="plan-top">
<h3>Starter <span class="tag-current">Current</span></h3>
<div class="price"><span class="amount">$0</span><span class="per">/mo</span></div>
</div>
<ul class="features">
<li><span class="ico ok">✓</span> Up to 5 seats</li>
<li><span class="ico ok">✓</span> 3 active projects</li>
<li><span class="ico no">–</span> Standard support</li>
<li><span class="ico no">×</span> Advanced analytics</li>
<li><span class="ico no">×</span> SSO & audit log</li>
</ul>
<button class="btn btn-ghost" type="button" disabled>Your plan</button>
</article>
<article class="plan recommended" role="listitem" data-plan="pro" tabindex="0" aria-pressed="true" data-monthly="29" data-annual="23">
<div class="ribbon">Recommended</div>
<div class="plan-top">
<h3>Pro</h3>
<div class="price"><span class="amount" data-price="pro">$29</span><span class="per" data-per>/seat·mo</span></div>
</div>
<ul class="features">
<li class="gain"><span class="ico ok">✓</span> Unlimited seats <span class="gain-tag">unlock</span></li>
<li class="gain"><span class="ico ok">✓</span> Unlimited projects <span class="gain-tag">unlock</span></li>
<li><span class="ico ok">✓</span> Priority support</li>
<li class="gain"><span class="ico ok">✓</span> Advanced analytics <span class="gain-tag">new</span></li>
<li><span class="ico no">×</span> SSO & audit log</li>
</ul>
<button class="btn btn-primary select-plan" type="button" data-plan="pro">Select Pro</button>
</article>
<article class="plan" role="listitem" data-plan="business" tabindex="0" aria-pressed="false" data-monthly="49" data-annual="39">
<div class="plan-top">
<h3>Business</h3>
<div class="price"><span class="amount" data-price="business">$49</span><span class="per" data-per>/seat·mo</span></div>
</div>
<ul class="features">
<li><span class="ico ok">✓</span> Everything in Pro</li>
<li><span class="ico ok">✓</span> Unlimited projects</li>
<li class="gain"><span class="ico ok">✓</span> Dedicated CSM <span class="gain-tag">new</span></li>
<li><span class="ico ok">✓</span> Advanced analytics</li>
<li class="gain"><span class="ico ok">✓</span> SSO & audit log <span class="gain-tag">new</span></li>
</ul>
<button class="btn btn-outline select-plan" type="button" data-plan="business">Select Business</button>
</article>
</div>
<footer class="summary" aria-live="polite">
<div class="summary-lines">
<div class="sum-row">
<span><strong id="sumPlan">Pro</strong> · <span id="sumSeats">8 seats</span> · <span id="sumPeriod">monthly</span></span>
<span id="sumBase">$232.00</span>
</div>
<div class="sum-row muted" id="prorationRow">
<span>Proration credit (remaining cycle)</span>
<span id="sumProration">−$11.40</span>
</div>
<div class="sum-row total">
<span>Due today</span>
<span id="sumTotal">$220.60</span>
</div>
<p class="proration-note" id="prorationNote">You'll be charged a prorated amount now, then <span id="renewAmount">$232.00/mo</span> on Jul 1.</p>
</div>
<div class="summary-actions">
<button class="btn btn-ghost" id="cancelBtn" type="button">Cancel</button>
<button class="btn btn-primary" id="confirmBtn" type="button">Confirm upgrade</button>
</div>
</footer>
</div>
<!-- SUCCESS VIEW -->
<div class="view view-success" id="viewSuccess" hidden>
<div class="success-inner">
<span class="success-check" aria-hidden="true">
<svg viewBox="0 0 24 24" width="34" height="34" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
</span>
<h2 id="successTitle" tabindex="-1">You're on <span id="successPlan">Pro</span> 🎉</h2>
<p class="success-sub">Your <span id="successSeats">8 seats</span> are active right now. Here's what your team just unlocked:</p>
<ul class="gain-list" id="gainList">
<li>Unlimited seats — invite the whole team</li>
<li>Unlimited projects</li>
<li>Advanced analytics dashboards</li>
<li>Priority support & faster response SLAs</li>
</ul>
<div class="success-actions">
<button class="btn btn-primary" id="doneBtn" type="button">Back to workspace</button>
</div>
</div>
</div>
</div>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Upgrade / Plan-compare Modal
A persuasive-but-honest upgrade flow for a fictional SaaS workspace. The page shell shows a team that has maxed out its Starter seats and projects, with a single CTA that opens a centered, accessible dialog. Inside, three plan cards sit side by side — the current plan, a recommended Pro tier, and Business — with every newly unlocked feature flagged by a coloured gain badge so customers can see precisely what they pay for.
Controls along the top drive a live order summary. A monthly/annual segmented toggle re-prices every card and the totals (annual shows the 20% saving), while a seat stepper lets buyers dial seats up or down with min/max clamping. The summary breaks down the base charge, a proration credit, and the amount due today, plus a plain-language renewal note — no hidden fees, no dark patterns.
Interaction is fully keyboard-friendly: the dialog traps focus, closes on Esc or backdrop click, and restores focus to the trigger. Choosing a plan and confirming runs a short processing state, then swaps to a success view that names the plan, confirms the seat count, and lists the concrete gains the team just unlocked.
Illustrative SaaS UI only — fictional product, metrics, and billing. No real backend.