Upsell — Usage-limit reached prompt
A self-contained usage-limit prompt for the fictional Northwind AI workspace, shown the moment a Starter plan exhausts its 1,000 monthly credits. A full red progress bar, the reset date, and an add-on balance line frame two CTAs — Upgrade plan and Buy add-on credits. The add-on panel carries a quantity stepper that recomputes credits and price live; purchasing adds credits back, shrinks the bar away from 100 percent, and surfaces a success card. An Upgrade modal lists Starter, Pro, and Scale tiers with a monthly versus annual toggle.
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-sm: 0 1px 2px rgba(16, 19, 34, 0.08);
--sh-md: 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:
radial-gradient(1100px 520px at 50% -180px, var(--brand-50), transparent 60%),
var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.page {
max-width: 720px;
margin: 0 auto;
padding: 0 20px 80px;
}
/* ---------- App bar ---------- */
.appbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 22px 4px 26px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
}
.brand-mark {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 10px;
background: linear-gradient(140deg, var(--brand), var(--brand-700));
color: #fff;
box-shadow: var(--sh-sm);
}
.brand-name {
font-weight: 800;
font-size: 16px;
letter-spacing: -0.01em;
}
.brand-name em {
font-style: normal;
color: var(--brand);
}
.appbar-meta {
display: inline-flex;
align-items: center;
gap: 12px;
}
.plan-pill {
font-size: 12px;
font-weight: 600;
color: var(--ink-2);
background: var(--white);
border: 1px solid var(--line);
padding: 5px 11px;
border-radius: 999px;
box-shadow: var(--sh-sm);
}
.avatar {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 50%;
background: var(--accent-soft);
color: #0a6b63;
font-size: 12px;
font-weight: 700;
border: 1px solid rgba(0, 180, 166, 0.25);
}
/* ---------- Buttons ---------- */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
font: inherit;
font-weight: 600;
font-size: 14px;
border: 1px solid transparent;
border-radius: var(--r-sm);
padding: 10px 16px;
cursor: pointer;
white-space: nowrap;
transition: background 0.15s ease, border-color 0.15s ease, transform 0.08s ease,
box-shadow 0.15s ease, color 0.15s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
.btn-lg {
padding: 12px 20px;
font-size: 15px;
}
.btn-sm {
padding: 8px 13px;
font-size: 13px;
}
.btn-block {
width: 100%;
}
.btn-primary {
background: var(--brand);
color: #fff;
box-shadow: var(--sh-sm);
}
.btn-primary:hover {
background: var(--brand-d);
}
.btn-outline {
background: var(--white);
color: var(--ink);
border-color: var(--line-2);
box-shadow: var(--sh-sm);
}
.btn-outline:hover {
border-color: var(--brand);
color: var(--brand-d);
}
.btn-outline:disabled {
color: var(--muted);
background: var(--bg);
border-color: var(--line);
cursor: not-allowed;
box-shadow: none;
}
.btn-ghost {
background: transparent;
color: var(--brand-d);
border-color: transparent;
}
.btn-ghost:hover {
background: var(--brand-50);
}
.btn-link {
background: none;
border: none;
padding: 0;
font: inherit;
font-weight: 600;
font-size: inherit;
color: var(--brand-d);
cursor: pointer;
text-decoration: underline;
text-underline-offset: 3px;
}
.btn-link:hover {
color: var(--brand-700);
}
/* ---------- Limit card ---------- */
.stage {
display: flex;
justify-content: center;
}
.limit-card {
position: relative;
width: 100%;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-md);
padding: 30px 30px 26px;
overflow: hidden;
}
.limit-card::before {
content: "";
position: absolute;
inset: 0 0 auto;
height: 4px;
background: linear-gradient(90deg, var(--danger), var(--warn));
transition: background 0.4s ease;
}
.limit-card.is-resolved::before {
background: linear-gradient(90deg, var(--ok), var(--accent));
}
.limit-flag {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.01em;
color: #9a2f22;
background: rgba(212, 80, 62, 0.1);
border: 1px solid rgba(212, 80, 62, 0.22);
padding: 5px 11px;
border-radius: 999px;
transition: color 0.3s ease, background 0.3s ease, border-color 0.3s ease;
}
.limit-flag.is-ok {
color: #1d6f4d;
background: rgba(47, 158, 111, 0.12);
border-color: rgba(47, 158, 111, 0.26);
}
.limit-title {
margin: 14px 0 8px;
font-size: clamp(21px, 4vw, 27px);
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.2;
}
.limit-sub {
margin: 0 0 22px;
color: var(--ink-2);
font-size: 15px;
max-width: 52ch;
}
.limit-sub strong {
color: var(--ink);
}
/* ---------- Usage meter ---------- */
.usage {
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 16px 18px 14px;
margin-bottom: 22px;
}
.usage-head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.usage-label {
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.usage-count {
font-size: 14px;
color: var(--muted);
font-variant-numeric: tabular-nums;
}
.usage-count strong {
font-size: 16px;
font-weight: 800;
color: var(--ink);
}
.usage-sep {
margin: 0 3px;
opacity: 0.55;
}
.bar {
position: relative;
height: 12px;
border-radius: 999px;
background: rgba(16, 19, 34, 0.08);
overflow: hidden;
}
.bar-fill {
height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, var(--brand), var(--accent));
transition: width 0.7s cubic-bezier(0.22, 0.61, 0.36, 1), background 0.4s ease;
}
.bar-fill.is-full {
background: linear-gradient(90deg, var(--danger), #e2724f);
}
.bar-fill.is-pulse {
animation: barPulse 1.1s ease-in-out infinite;
}
@keyframes barPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.usage-foot {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 6px 14px;
margin: 12px 0 0;
font-size: 13px;
color: var(--muted);
}
.reset,
.balance {
display: inline-flex;
align-items: center;
gap: 7px;
}
.reset strong,
.balance strong {
color: var(--ink-2);
font-weight: 700;
}
.reset svg {
color: var(--muted);
}
.reset-rel {
opacity: 0.8;
}
.balance {
color: #1d6f4d;
font-weight: 600;
}
.balance strong {
color: #1d6f4d;
}
.balance-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--ok);
box-shadow: 0 0 0 3px rgba(47, 158, 111, 0.18);
}
/* ---------- CTA row ---------- */
.cta-row {
display: flex;
gap: 12px;
}
.cta-row .btn {
flex: 1;
}
/* ---------- Add-on panel ---------- */
.addon-panel {
margin-top: 18px;
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--white);
padding: 18px;
animation: panelIn 0.25s ease;
}
@keyframes panelIn {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
.addon-grid {
display: grid;
grid-template-columns: 1fr 230px;
gap: 20px;
align-items: start;
}
.addon-title {
margin: 0 0 4px;
font-size: 15px;
font-weight: 700;
}
.addon-note {
margin: 0 0 16px;
font-size: 13px;
color: var(--muted);
}
.stepper {
display: inline-flex;
align-items: stretch;
border: 1px solid var(--line-2);
border-radius: var(--r-md);
overflow: hidden;
background: var(--white);
}
.step-btn {
display: grid;
place-items: center;
width: 46px;
background: var(--bg);
border: none;
color: var(--ink-2);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.step-btn:hover:not(:disabled) {
background: var(--brand-50);
color: var(--brand-d);
}
.step-btn:disabled {
color: var(--line-2);
cursor: not-allowed;
}
.step-btn:focus-visible {
outline: 2px solid var(--brand);
outline-offset: -2px;
}
.step-readout {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding: 8px 6px;
min-width: 118px;
border-left: 1px solid var(--line);
border-right: 1px solid var(--line);
}
.step-input {
width: 100%;
border: none;
background: none;
text-align: center;
font: inherit;
font-weight: 800;
font-size: 20px;
color: var(--ink);
font-variant-numeric: tabular-nums;
padding: 0;
}
.step-input:focus-visible {
outline: none;
color: var(--brand-d);
}
.step-unit {
font-size: 11px;
color: var(--muted);
white-space: nowrap;
}
/* ---------- Summary ---------- */
.addon-right {
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 14px;
}
.summary {
margin: 0 0 12px;
}
.summary-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
font-size: 13px;
color: var(--muted);
padding: 4px 0;
}
.summary-row dt,
.summary-row dd {
margin: 0;
}
.summary-row dd {
font-weight: 600;
color: var(--ink-2);
font-variant-numeric: tabular-nums;
}
.summary-total {
margin-top: 4px;
padding-top: 10px;
border-top: 1px solid var(--line);
font-size: 14px;
}
.summary-total dt {
font-weight: 700;
color: var(--ink);
}
.summary-total dd {
font-size: 18px;
font-weight: 800;
color: var(--ink);
}
.addon-secure {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 10px 0 0;
font-size: 11px;
color: var(--muted);
}
/* ---------- Success state ---------- */
.success {
display: flex;
align-items: center;
gap: 14px;
margin-top: 18px;
padding: 14px 16px;
border-radius: var(--r-md);
background: rgba(47, 158, 111, 0.09);
border: 1px solid rgba(47, 158, 111, 0.24);
animation: panelIn 0.3s ease;
}
.success-icon {
display: grid;
place-items: center;
flex: none;
width: 38px;
height: 38px;
border-radius: 50%;
background: var(--ok);
color: #fff;
animation: pop 0.4s cubic-bezier(0.18, 1.4, 0.5, 1);
}
@keyframes pop {
0% { transform: scale(0.4); opacity: 0; }
100% { transform: scale(1); opacity: 1; }
}
.success-copy {
margin: 0;
display: flex;
flex-direction: column;
flex: 1;
}
.success-copy strong {
font-size: 14px;
color: #1d6f4d;
}
.success-copy span {
font-size: 13px;
color: var(--ink-2);
}
/* ---------- Foot ---------- */
.limit-foot {
margin: 22px 0 0;
font-size: 13px;
color: var(--muted);
}
/* ---------- Modal ---------- */
.overlay {
position: fixed;
inset: 0;
background: rgba(16, 19, 34, 0.46);
backdrop-filter: blur(2px);
z-index: 40;
animation: fade 0.18s ease;
}
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
.modal {
position: fixed;
z-index: 50;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(720px, calc(100vw - 32px));
max-height: calc(100vh - 40px);
overflow: auto;
background: var(--surface);
border-radius: var(--r-lg);
box-shadow: 0 24px 70px rgba(16, 19, 34, 0.28);
padding: 26px;
animation: rise 0.22s cubic-bezier(0.22, 0.61, 0.36, 1);
}
@keyframes rise {
from { opacity: 0; transform: translate(-50%, -46%); }
to { opacity: 1; transform: translate(-50%, -50%); }
}
.modal-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.modal-title {
margin: 0 0 4px;
font-size: 21px;
font-weight: 800;
letter-spacing: -0.01em;
}
.modal-sub {
margin: 0;
color: var(--muted);
font-size: 14px;
}
.icon-btn {
display: grid;
place-items: center;
flex: none;
width: 36px;
height: 36px;
border-radius: var(--r-sm);
border: 1px solid var(--line);
background: var(--white);
color: var(--ink-2);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.icon-btn:hover {
background: var(--bg);
color: var(--ink);
}
.icon-btn:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
/* ---------- Billing cycle toggle ---------- */
.cycle {
display: inline-flex;
padding: 4px;
gap: 4px;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 999px;
margin-bottom: 18px;
}
.cycle-opt {
display: inline-flex;
align-items: center;
gap: 8px;
font: inherit;
font-weight: 600;
font-size: 13px;
color: var(--ink-2);
background: transparent;
border: none;
padding: 7px 16px;
border-radius: 999px;
cursor: pointer;
transition: background 0.18s ease, color 0.18s ease, box-shadow 0.18s ease;
}
.cycle-opt.is-active {
background: var(--white);
color: var(--ink);
box-shadow: var(--sh-sm);
}
.cycle-opt:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
}
.cycle-save {
font-size: 11px;
font-weight: 700;
color: #0a6b63;
background: var(--accent-soft);
padding: 2px 7px;
border-radius: 999px;
}
/* ---------- Plans ---------- */
.plans {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
}
.plan {
position: relative;
display: flex;
flex-direction: column;
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 18px 16px;
background: var(--white);
transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
}
.plan:hover {
border-color: var(--line-2);
box-shadow: var(--sh-sm);
}
.plan-popular {
border-color: var(--brand);
box-shadow: 0 0 0 1px var(--brand), var(--sh-md);
}
.plan-badge {
position: absolute;
top: -11px;
left: 50%;
transform: translateX(-50%);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.02em;
color: #fff;
background: var(--brand);
padding: 3px 11px;
border-radius: 999px;
white-space: nowrap;
}
.plan-name {
margin: 0 0 2px;
font-size: 16px;
font-weight: 800;
}
.plan-desc {
margin: 0 0 12px;
font-size: 12.5px;
color: var(--muted);
min-height: 2.4em;
}
.plan-price {
margin: 0 0 2px;
display: flex;
align-items: baseline;
gap: 3px;
}
.plan-price .amount {
font-size: 28px;
font-weight: 800;
letter-spacing: -0.02em;
}
.plan-price .per {
font-size: 13px;
color: var(--muted);
font-weight: 600;
}
.plan-credits {
margin: 0 0 14px;
font-size: 12.5px;
font-weight: 600;
color: var(--brand-d);
}
.features {
list-style: none;
margin: 0 0 16px;
padding: 0;
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
}
.features li {
position: relative;
padding-left: 22px;
font-size: 13px;
color: var(--ink-2);
}
.features li::before {
content: "";
position: absolute;
left: 0;
top: 3px;
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--accent-soft);
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 24 24' fill='none' stroke='%2300b4a6' stroke-width='3.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4 12 5 5L20 6'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center;
}
.plan-cta {
width: 100%;
}
.modal-foot {
margin: 18px 0 0;
text-align: center;
font-size: 12.5px;
color: var(--muted);
}
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
z-index: 60;
display: flex;
flex-direction: column;
gap: 10px;
width: max-content;
max-width: calc(100vw - 32px);
pointer-events: none;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
background: var(--ink);
color: #fff;
font-size: 13.5px;
font-weight: 500;
padding: 11px 16px;
border-radius: var(--r-sm);
box-shadow: var(--sh-md);
animation: toastIn 0.25s ease;
}
.toast.is-out {
animation: toastOut 0.25s ease forwards;
}
.toast-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
flex: none;
}
@keyframes toastIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toastOut {
to { opacity: 0; transform: translateY(12px); }
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.page {
padding: 0 14px 60px;
}
.appbar {
padding: 16px 2px 20px;
}
.plan-pill {
display: none;
}
.limit-card {
padding: 22px 18px 20px;
}
.cta-row {
flex-direction: column;
}
.addon-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.stepper {
width: 100%;
}
.step-readout {
flex: 1;
}
.success {
flex-wrap: wrap;
}
.success .btn {
width: 100%;
}
.modal {
padding: 20px 16px;
}
.plans {
grid-template-columns: 1fr;
gap: 12px;
}
.plan-desc {
min-height: 0;
}
.plan-popular {
order: -1;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.001ms !important;
}
}(function () {
"use strict";
/* ---------- Config / state ---------- */
var QUOTA = 1000; // monthly credit allowance
var PACK_SIZE = 500; // credits per add-on pack
var PACK_PRICE = 9; // dollars per pack
var MAX_PACKS = 20;
var MIN_PACKS = 1;
var state = {
used: QUOTA, // credits consumed this period (starts at the cap)
addon: 0, // purchased add-on credits available
qty: 1, // packs selected in the stepper
cycle: "monthly"
};
var PRICES = {
monthly: { starter: "$0", pro: "$29", scale: "$99" },
annual: { starter: "$0", pro: "$23", scale: "$79" }
};
/* ---------- Helpers ---------- */
var $ = function (id) { return document.getElementById(id); };
var fmt = function (n) { return n.toLocaleString("en-US"); };
var money = function (n) {
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
};
var toastWrap = $("toastWrap");
function toast(msg) {
var el = document.createElement("div");
el.className = "toast";
el.innerHTML = '<span class="toast-dot" aria-hidden="true"></span>' +
"<span></span>";
el.lastChild.textContent = msg;
toastWrap.appendChild(el);
setTimeout(function () {
el.classList.add("is-out");
el.addEventListener("animationend", function () { el.remove(); });
}, 2600);
}
/* ---------- Usage meter rendering ---------- */
var barFill = $("barFill");
var usageBar = $("usageBar");
var usedNum = $("usedNum");
var totalNum = $("totalNum");
var balanceWrap = $("balanceWrap");
var balanceNum = $("balanceNum");
var limitFlag = $("limitFlag");
var limitFlagText = $("limitFlagText");
var limitCard = $("limitCard");
var limitTitle = $("limitTitle");
var limitSub = $("limitSub");
function renderMeter() {
var total = QUOTA + state.addon;
var pct = total > 0 ? Math.min(100, (state.used / total) * 100) : 100;
var atLimit = state.used >= total;
barFill.style.width = pct + "%";
barFill.classList.toggle("is-full", atLimit);
usedNum.textContent = fmt(state.used);
totalNum.textContent = fmt(total);
usageBar.setAttribute("aria-valuemax", String(total));
usageBar.setAttribute("aria-valuenow", String(state.used));
// Add-on balance line
if (state.addon > 0) {
balanceNum.textContent = fmt(state.addon);
balanceWrap.hidden = false;
} else {
balanceWrap.hidden = true;
}
// Flag + headline shift once the user is no longer blocked
if (atLimit) {
limitFlag.classList.remove("is-ok");
limitFlagText.textContent = "Limit reached";
limitCard.classList.remove("is-resolved");
limitTitle.textContent = "You've used all " + fmt(total) + " of your monthly credits";
} else {
limitFlag.classList.add("is-ok");
limitFlagText.textContent = fmt(total - state.used) + " credits available";
limitCard.classList.add("is-resolved");
limitTitle.textContent = "You're back under your limit";
limitSub.innerHTML =
"Your add-on credits are live. Generation is enabled again — upgrade any time for a higher monthly allowance.";
}
}
/* ---------- Stepper / purchase summary ---------- */
var qtyInput = $("qtyInput");
var incBtn = $("incBtn");
var decBtn = $("decBtn");
var packCredits = $("packCredits");
var sumCredits = $("sumCredits");
var sumQty = $("sumQty");
var sumSubtotal = $("sumSubtotal");
var sumTotal = $("sumTotal");
var purchasePrice = $("purchasePrice");
packCredits.textContent = fmt(PACK_SIZE);
function clampQty(n) {
if (isNaN(n) || n < MIN_PACKS) n = MIN_PACKS;
if (n > MAX_PACKS) n = MAX_PACKS;
return Math.floor(n);
}
function renderSummary() {
var credits = state.qty * PACK_SIZE;
var total = state.qty * PACK_PRICE;
qtyInput.value = String(state.qty);
sumCredits.textContent = fmt(credits);
sumQty.textContent = String(state.qty);
sumSubtotal.textContent = money(total);
sumTotal.textContent = money(total);
purchasePrice.textContent = money(total);
decBtn.disabled = state.qty <= MIN_PACKS;
incBtn.disabled = state.qty >= MAX_PACKS;
}
function setQty(n) {
state.qty = clampQty(n);
renderSummary();
}
incBtn.addEventListener("click", function () { setQty(state.qty + 1); });
decBtn.addEventListener("click", function () { setQty(state.qty - 1); });
qtyInput.addEventListener("input", function () {
var v = parseInt(qtyInput.value.replace(/[^0-9]/g, ""), 10);
if (!isNaN(v)) { state.qty = clampQty(v); renderSummary(); }
});
qtyInput.addEventListener("blur", function () { setQty(state.qty); });
qtyInput.addEventListener("keydown", function (e) {
if (e.key === "ArrowUp") { e.preventDefault(); setQty(state.qty + 1); }
if (e.key === "ArrowDown") { e.preventDefault(); setQty(state.qty - 1); }
});
/* ---------- Add-on panel toggle ---------- */
var addonBtn = $("addonBtn");
var addonPanel = $("addonPanel");
addonBtn.addEventListener("click", function () {
var open = !addonPanel.hidden;
if (open) {
addonPanel.hidden = true;
addonBtn.setAttribute("aria-expanded", "false");
} else {
addonPanel.hidden = false;
addonBtn.setAttribute("aria-expanded", "true");
addonPanel.scrollIntoView({ behavior: "smooth", block: "nearest" });
qtyInput.focus();
}
});
/* ---------- Purchase → success state ---------- */
var purchaseBtn = $("purchaseBtn");
var successBox = $("successBox");
var successHead = $("successHead");
var successSub = $("successSub");
var resumeBtn = $("resumeBtn");
purchaseBtn.addEventListener("click", function () {
var addedCredits = state.qty * PACK_SIZE;
var addedQty = state.qty;
// Grant the credits to the balance and free up headroom in the meter.
state.addon += addedCredits;
// "Add credits back" — the consumed count stays, but total grows, so the
// bar shrinks away from 100%.
renderMeter();
// Collapse the purchase panel, surface a success card.
addonPanel.hidden = true;
addonBtn.setAttribute("aria-expanded", "false");
successHead.textContent = fmt(addedCredits) + " credits added";
successSub.textContent =
addedQty + " pack" + (addedQty === 1 ? "" : "s") + " purchased · new balance " +
fmt(state.addon) + " add-on credits.";
successBox.hidden = false;
successBox.scrollIntoView({ behavior: "smooth", block: "nearest" });
// Briefly pulse the bar to draw the eye to the change.
barFill.classList.add("is-pulse");
setTimeout(function () { barFill.classList.remove("is-pulse"); }, 1400);
toast("Purchase complete — " + fmt(addedCredits) + " credits added");
// Reset the stepper for any subsequent top-up.
setQty(1);
});
resumeBtn.addEventListener("click", function () {
toast("Picking up where you left off…");
successBox.hidden = true;
});
/* ---------- Plans modal ---------- */
var overlay = $("overlay");
var modal = $("modal");
var modalClose = $("modalClose");
var upgradeBtn = $("upgradeBtn");
var contactBtn = $("contactBtn");
var lastFocus = null;
function openModal() {
lastFocus = document.activeElement;
overlay.hidden = false;
modal.hidden = false;
document.body.style.overflow = "hidden";
modalClose.focus();
document.addEventListener("keydown", onKeydown);
}
function closeModal() {
overlay.hidden = true;
modal.hidden = true;
document.body.style.overflow = "";
document.removeEventListener("keydown", onKeydown);
if (lastFocus && lastFocus.focus) lastFocus.focus();
}
function onKeydown(e) {
if (e.key === "Escape") { closeModal(); return; }
if (e.key === "Tab") {
var f = modal.querySelectorAll(
'button:not([disabled]), [href], input, [tabindex]:not([tabindex="-1"])'
);
if (!f.length) return;
var first = f[0];
var last = f[f.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault(); last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault(); first.focus();
}
}
}
upgradeBtn.addEventListener("click", openModal);
contactBtn.addEventListener("click", function () {
toast("Sales request sent — we'll be in touch shortly");
});
modalClose.addEventListener("click", closeModal);
overlay.addEventListener("click", closeModal);
/* ---------- Billing cycle toggle ---------- */
var cycleOpts = modal.querySelectorAll(".cycle-opt");
var cycleNote = $("cycleNote");
function applyCycle(cycle) {
state.cycle = cycle;
var map = PRICES[cycle];
var amounts = modal.querySelectorAll(".amount[data-price]");
amounts.forEach(function (el) {
var key = el.getAttribute("data-price");
if (map[key]) el.textContent = map[key];
});
modal.querySelectorAll(".per[data-per]").forEach(function (el) {
el.textContent = cycle === "annual" ? "/mo, billed yearly" : "/mo";
});
cycleNote.textContent = cycle === "annual" ? "annually" : "monthly";
cycleOpts.forEach(function (b) {
var active = b.getAttribute("data-cycle") === cycle;
b.classList.toggle("is-active", active);
b.setAttribute("aria-pressed", active ? "true" : "false");
});
}
cycleOpts.forEach(function (b) {
b.addEventListener("click", function () {
applyCycle(b.getAttribute("data-cycle"));
});
});
/* ---------- Plan selection ---------- */
modal.querySelectorAll(".plan-cta[data-plan]").forEach(function (b) {
b.addEventListener("click", function () {
var plan = b.getAttribute("data-plan");
closeModal();
toast("Upgraded to " + plan + " — your new allowance is active");
});
});
/* ---------- Init ---------- */
renderMeter();
renderSummary();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northwind — Usage-Limit Reached Prompt</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">
<!-- Context header to ground the prompt in a product shell -->
<header class="appbar">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="m13 2-9 12h7l-2 8 9-12h-7l2-8Z" />
</svg>
</span>
<span class="brand-name">Northwind <em>AI</em></span>
</div>
<nav class="appbar-meta" aria-label="Workspace">
<span class="plan-pill">Starter plan</span>
<span class="avatar" aria-hidden="true">RO</span>
</nav>
</header>
<main class="stage">
<!-- Usage-limit prompt card -->
<section class="limit-card" id="limitCard" role="region" aria-labelledby="limitTitle">
<span class="limit-flag" id="limitFlag">
<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 9v4M12 17h.01" />
<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" />
</svg>
<span id="limitFlagText">Limit reached</span>
</span>
<h1 class="limit-title" id="limitTitle">You've used all 1,000 of your monthly credits</h1>
<p class="limit-sub" id="limitSub">
Generation is paused on the <strong>Starter</strong> plan. Upgrade for a higher allowance,
or top up with add-on credits to keep going right now.
</p>
<!-- Usage meter -->
<div class="usage">
<div class="usage-head">
<span class="usage-label">Monthly credits</span>
<span class="usage-count">
<strong id="usedNum" aria-live="polite" aria-atomic="true">1,000</strong>
<span class="usage-sep">/</span>
<span id="totalNum">1,000</span>
</span>
</div>
<div class="bar" role="progressbar" id="usageBar" aria-valuemin="0" aria-valuemax="1000" aria-valuenow="1000" aria-label="Monthly credits used">
<div class="bar-fill is-full" id="barFill" style="width:100%"></div>
</div>
<p class="usage-foot">
<span class="reset">
<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 12a9 9 0 1 0 3-6.7L3 8" /><path d="M3 3v5h5" />
</svg>
Resets <strong>Jul 1, 2026</strong> <span class="reset-rel">(in 18 days)</span>
</span>
<span class="balance" id="balanceWrap" hidden>
<span class="balance-dot" aria-hidden="true"></span>
<strong id="balanceNum">0</strong> add-on credits available
</span>
</p>
</div>
<!-- Primary CTAs -->
<div class="cta-row">
<button class="btn btn-primary btn-lg" id="upgradeBtn" type="button">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 19V5M5 12l7-7 7 7" />
</svg>
Upgrade plan
</button>
<button class="btn btn-outline btn-lg" id="addonBtn" type="button" aria-expanded="false" aria-controls="addonPanel">
<svg viewBox="0 0 24 24" width="17" height="17" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<circle cx="12" cy="12" r="9" /><path d="M12 8v8M8 12h8" />
</svg>
Buy add-on credits
</button>
</div>
<!-- Add-on purchase panel (stepper) -->
<div class="addon-panel" id="addonPanel" hidden>
<div class="addon-grid">
<div class="addon-left">
<p class="addon-title">Add-on credit packs</p>
<p class="addon-note">One-time purchase. Credits roll over until used — no expiry.</p>
<div class="stepper" role="group" aria-label="Add-on pack quantity">
<button class="step-btn" id="decBtn" type="button" aria-label="Decrease quantity">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M5 12h14" /></svg>
</button>
<div class="step-readout">
<input class="step-input" id="qtyInput" type="text" inputmode="numeric" value="1" aria-label="Number of packs" />
<span class="step-unit"><span id="packCredits">500</span> credits / pack</span>
</div>
<button class="step-btn" id="incBtn" type="button" aria-label="Increase quantity">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
</button>
</div>
</div>
<div class="addon-right">
<dl class="summary">
<div class="summary-row">
<dt>Credits added</dt>
<dd id="sumCredits">500</dd>
</div>
<div class="summary-row">
<dt><span id="sumQty">1</span> × $9.00</dt>
<dd id="sumSubtotal">$9.00</dd>
</div>
<div class="summary-row summary-total">
<dt>Total today</dt>
<dd id="sumTotal">$9.00</dd>
</div>
</dl>
<button class="btn btn-primary btn-block" id="purchaseBtn" type="button">
Purchase <span id="purchasePrice">$9.00</span>
</button>
<p class="addon-secure">
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="4" y="11" width="16" height="9" rx="2" /><path d="M8 11V8a4 4 0 0 1 8 0v3" />
</svg>
Secure checkout · billed once
</p>
</div>
</div>
</div>
<!-- Success state (revealed after purchase) -->
<div class="success" id="successBox" hidden role="status" aria-live="polite">
<span class="success-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">
<path d="m4 12 5 5L20 6" />
</svg>
</span>
<p class="success-copy">
<strong id="successHead">Credits added</strong>
<span id="successSub">Your balance is topped up — pick up right where you left off.</span>
</p>
<button class="btn btn-ghost btn-sm" id="resumeBtn" type="button">Resume work</button>
</div>
<p class="limit-foot">
Need a custom volume?
<button class="btn btn-link" id="contactBtn" type="button">Talk to sales</button>
</p>
</section>
</main>
<!-- Plans modal -->
<div class="overlay" id="overlay" hidden></div>
<div class="modal" id="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle" hidden>
<div class="modal-head">
<div>
<h2 class="modal-title" id="modalTitle">Upgrade for more credits</h2>
<p class="modal-sub">Higher monthly allowances, faster models, and team seats.</p>
</div>
<button class="icon-btn" id="modalClose" 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" stroke-linejoin="round">
<path d="m6 6 12 12M18 6 6 18" />
</svg>
</button>
</div>
<div class="cycle" role="group" aria-label="Billing cycle">
<button class="cycle-opt is-active" data-cycle="monthly" type="button" aria-pressed="true">Monthly</button>
<button class="cycle-opt" data-cycle="annual" type="button" aria-pressed="false">
Annual <span class="cycle-save">Save 20%</span>
</button>
</div>
<div class="plans">
<article class="plan">
<h3 class="plan-name">Starter</h3>
<p class="plan-desc">Where you are now.</p>
<p class="plan-price"><span class="amount" data-price="starter">$0</span><span class="per" data-per>/mo</span></p>
<p class="plan-credits">1,000 credits / mo</p>
<ul class="features">
<li>Standard model</li>
<li>1 seat</li>
<li>Community support</li>
</ul>
<button class="btn btn-outline plan-cta" data-plan="Starter" type="button" disabled>Current plan</button>
</article>
<article class="plan plan-popular">
<span class="plan-badge">Most popular</span>
<h3 class="plan-name">Pro</h3>
<p class="plan-desc">For daily, serious work.</p>
<p class="plan-price"><span class="amount" data-price="pro">$29</span><span class="per" data-per>/mo</span></p>
<p class="plan-credits">25,000 credits / mo</p>
<ul class="features">
<li>Priority & fast models</li>
<li>5 seats</li>
<li>Rollover credits</li>
<li>Email support</li>
</ul>
<button class="btn btn-primary plan-cta" data-plan="Pro" type="button">Choose Pro</button>
</article>
<article class="plan">
<h3 class="plan-name">Scale</h3>
<p class="plan-desc">For teams & pipelines.</p>
<p class="plan-price"><span class="amount" data-price="scale">$99</span><span class="per" data-per>/mo</span></p>
<p class="plan-credits">120,000 credits / mo</p>
<ul class="features">
<li>Everything in Pro</li>
<li>Up to 25 seats</li>
<li>SSO & audit log</li>
<li>Priority support</li>
</ul>
<button class="btn btn-outline plan-cta" data-plan="Scale" type="button">Choose Scale</button>
</article>
</div>
<p class="modal-foot">Prices in USD · billed <span id="cycleNote">monthly</span> · cancel anytime.</p>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
</div>
<script src="script.js"></script>
</body>
</html>Usage-limit reached prompt
A complete quota-exhausted prompt for the fictional Northwind AI workspace, art-directed in the
neutral product palette with an indigo brand and teal accent. A compact app bar establishes the
product shell, then a single card delivers the message: “You’ve used all 1,000 of your monthly
credits.” A usage meter sits at 100% in a red-to-amber fill, the reset date reads “Jul 1, 2026 (in
18 days)”, and an aria-live count keeps the used-versus-total figure announced. Two CTAs anchor the
card — a primary Upgrade plan and an outline Buy add-on credits.
Choosing add-on credits expands an inline panel with a quantity stepper (typeable input plus +/− buttons and arrow-key support, clamped 1–20 packs). The stepper recomputes the credits granted, the line subtotal, and the total live, mirrored into the purchase button label. Completing a purchase grows the total allowance, so the bar visibly shrinks away from 100%, the consumed count drops below the cap, and a green success card appears reporting the new balance — a toast confirms each action. The flag, top border, and headline all flip from a red “Limit reached” state to a green “credits available” state once the user is unblocked.
An Upgrade plan modal (role="dialog", aria-modal, focus trap, Esc to close) presents Starter,
a highlighted “Most popular” Pro, and Scale tiers with per-plan credit allowances, feature lists, and
a Monthly/Annual toggle that swaps every price live with a “Save 20%” badge. Everything is vanilla
HTML, CSS, and JS with inline SVG icons — no frameworks, no build step, and no network requests
beyond the Inter web font. A prefers-reduced-motion block neutralizes the animations, and the
layout collapses to a single column below 520px.
Illustrative UI only — the brand, plans, credits, and prices are fictional; not a real product.