Upsell — Win-back / discount offer modal
A self-contained exit-intent win-back modal for the fictional Northwind Cloud. As a user abandons checkout, a celebratory dialog offers 30 percent off their first three months — a spinning starburst discount badge, a strikethrough comparison of the standard versus discounted monthly price, and a copyable promo code field with a copied toast. A live urgency countdown ticks toward expiry, while Claim offer and No thanks actions, a focus trap, and Escape-to-close keep it fully keyboard accessible.
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);
--sh-3: 0 24px 60px rgba(16, 19, 34, 0.22);
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font-size: 16px;
line-height: 1.5;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.page {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ---------- App shell (background context) ---------- */
.appbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px clamp(16px, 5vw, 40px);
background: var(--surface);
border-bottom: 1px solid var(--line);
box-shadow: var(--sh-1);
}
.brand {
display: inline-flex;
align-items: center;
gap: 9px;
font-weight: 700;
letter-spacing: -0.01em;
}
.brand-mark {
display: inline-grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 9px;
color: var(--white);
background: linear-gradient(135deg, var(--brand), var(--brand-700));
box-shadow: var(--sh-1);
}
.brand-name {
font-size: 1.05rem;
}
.brand-name em {
font-style: normal;
color: var(--brand);
}
.appnav {
display: flex;
align-items: center;
gap: 6px;
}
.nav-link {
padding: 8px 12px;
border-radius: var(--r-sm);
color: var(--ink-2);
text-decoration: none;
font-weight: 500;
font-size: 0.94rem;
transition: background 0.15s ease, color 0.15s ease;
}
.nav-link:hover {
background: var(--brand-50);
color: var(--brand-d);
}
.shell {
flex: 1;
display: grid;
place-items: center;
padding: clamp(28px, 6vw, 64px) clamp(16px, 5vw, 40px);
}
.hero {
width: 100%;
max-width: 520px;
text-align: center;
}
.eyebrow {
margin: 0 0 10px;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--brand);
}
.hero-title {
margin: 0 0 12px;
font-size: clamp(1.7rem, 4.5vw, 2.3rem);
font-weight: 800;
letter-spacing: -0.025em;
line-height: 1.12;
}
.hero-sub {
margin: 0 auto 26px;
max-width: 44ch;
color: var(--muted);
font-size: 1rem;
}
.summary {
text-align: left;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px;
box-shadow: var(--sh-2);
}
.summary-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 12px;
padding: 10px 0;
}
.summary-row + .summary-row {
border-top: 1px solid var(--line);
}
.summary-label {
font-weight: 600;
color: var(--ink-2);
}
.summary-val {
font-weight: 700;
font-size: 1.05rem;
}
.summary-val.muted {
font-weight: 500;
color: var(--muted);
font-size: 0.95rem;
}
.summary-val .per {
font-size: 0.8rem;
font-weight: 600;
color: var(--muted);
}
.summary-total .summary-label,
.summary-total .summary-val {
font-size: 1.1rem;
color: var(--ink);
}
.summary-foot {
margin: 12px 0 0;
text-align: center;
font-size: 0.82rem;
color: var(--muted);
}
.hint {
margin: 22px auto 0;
max-width: 46ch;
font-size: 0.9rem;
color: var(--muted);
}
/* ---------- Buttons ---------- */
.btn {
font: inherit;
font-weight: 600;
cursor: pointer;
border: 1px solid transparent;
border-radius: var(--r-sm);
padding: 10px 16px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
line-height: 1.1;
transition: transform 0.12s ease, background 0.15s ease, color 0.15s ease,
border-color 0.15s ease, box-shadow 0.15s ease;
}
.btn:active {
transform: translateY(1px);
}
.btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--white), 0 0 0 5px var(--brand);
}
.btn-primary {
color: var(--white);
background: linear-gradient(135deg, var(--brand), var(--brand-d));
box-shadow: 0 6px 18px rgba(91, 91, 240, 0.32);
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--brand-d), var(--brand-700));
box-shadow: 0 8px 22px rgba(91, 91, 240, 0.4);
}
.btn-ghost {
background: transparent;
color: var(--ink-2);
border-color: var(--line-2);
}
.btn-ghost:hover {
background: var(--bg);
color: var(--ink);
}
.btn-link {
background: none;
border: none;
padding: 0;
color: var(--brand-d);
font-weight: 600;
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
}
.btn-link:hover {
color: var(--brand-700);
}
.btn-lg {
padding: 14px 20px;
font-size: 1.02rem;
border-radius: var(--r-md);
}
.btn-block {
width: 100%;
}
.icon-btn {
display: inline-grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 50%;
border: 1px solid var(--line);
background: var(--surface);
color: var(--ink-2);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, transform 0.12s ease;
}
.icon-btn:hover {
background: var(--bg);
color: var(--ink);
}
.icon-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--white), 0 0 0 5px var(--brand);
}
/* ---------- Overlay + modal ---------- */
.overlay {
position: fixed;
inset: 0;
background: rgba(16, 19, 34, 0.5);
backdrop-filter: blur(3px);
-webkit-backdrop-filter: blur(3px);
z-index: 40;
animation: fade-in 0.2s ease both;
}
.modal {
position: fixed;
z-index: 50;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: min(440px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
overflow: auto;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-3);
padding: 44px 28px 24px;
text-align: center;
animation: pop-in 0.26s cubic-bezier(0.18, 0.9, 0.32, 1.2) both;
}
.modal-glow {
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
background: radial-gradient(
120% 70% at 50% -10%,
rgba(0, 180, 166, 0.16),
transparent 60%
),
radial-gradient(120% 80% at 50% -20%, rgba(91, 91, 240, 0.12), transparent 60%);
}
.modal > *:not(.modal-glow) {
position: relative;
}
.modal-close {
position: absolute;
top: 12px;
right: 12px;
width: 34px;
height: 34px;
z-index: 2;
}
/* Discount badge burst */
.badge-burst {
position: relative;
width: 92px;
height: 92px;
margin: 0 auto 14px;
display: grid;
place-items: center;
}
.burst-rays {
position: absolute;
inset: 0;
color: var(--accent);
animation: ray-spin 18s linear infinite;
}
.badge-disc {
position: relative;
display: grid;
place-items: center;
width: 76px;
height: 76px;
border-radius: 50%;
color: var(--white);
background: linear-gradient(140deg, var(--accent), #019a8e);
box-shadow: 0 10px 24px rgba(0, 180, 166, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.35);
line-height: 1;
}
.disc-num {
font-size: 1.5rem;
font-weight: 800;
letter-spacing: -0.03em;
}
.disc-word {
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.18em;
margin-top: 1px;
}
.modal-eyebrow {
margin: 0 0 6px;
font-size: 0.76rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--accent);
}
.modal-title {
margin: 0 0 8px;
font-size: 1.4rem;
font-weight: 800;
letter-spacing: -0.02em;
line-height: 1.18;
}
.modal-sub {
margin: 0 auto 20px;
max-width: 40ch;
color: var(--muted);
font-size: 0.95rem;
}
/* Price comparison */
.price-compare {
display: flex;
align-items: stretch;
justify-content: center;
gap: 12px;
margin: 0 0 18px;
}
.price-block {
flex: 1;
border-radius: var(--r-md);
padding: 14px 12px;
border: 1px solid var(--line);
display: flex;
flex-direction: column;
gap: 4px;
align-items: center;
justify-content: center;
}
.price-old {
background: var(--bg);
color: var(--muted);
}
.price-new {
background: var(--accent-soft);
border-color: rgba(0, 180, 166, 0.35);
color: var(--ink);
}
.price-cap {
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
opacity: 0.85;
}
.price-amt {
font-size: 1.45rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.price-old .price-amt s {
color: var(--muted);
}
.price-per {
font-size: 0.78rem;
font-weight: 600;
color: var(--muted);
margin-left: 1px;
}
.price-new .price-per {
color: var(--ink-2);
}
.price-save {
margin-top: 2px;
font-size: 0.74rem;
font-weight: 700;
color: #018073;
background: var(--white);
border-radius: 999px;
padding: 2px 8px;
}
.price-arrow {
display: grid;
place-items: center;
color: var(--accent);
flex: 0 0 auto;
}
/* Promo code */
.promo {
text-align: left;
margin: 0 0 16px;
}
.promo-label {
display: block;
font-size: 0.78rem;
font-weight: 700;
color: var(--ink-2);
margin-bottom: 6px;
letter-spacing: 0.02em;
}
.promo-field {
display: flex;
align-items: stretch;
gap: 6px;
border: 1.5px dashed var(--line-2);
border-radius: var(--r-md);
padding: 6px;
background: var(--bg);
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.promo-field.is-copied {
border-color: var(--accent);
border-style: solid;
box-shadow: 0 0 0 3px rgba(0, 180, 166, 0.18);
}
.promo-input {
flex: 1;
min-width: 0;
border: none;
background: transparent;
font: inherit;
font-weight: 700;
letter-spacing: 0.12em;
color: var(--ink);
padding: 8px 10px;
font-size: 1rem;
}
.promo-input:focus-visible {
outline: none;
}
.promo-copy {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 6px;
border: none;
cursor: pointer;
font: inherit;
font-weight: 700;
font-size: 0.88rem;
padding: 8px 14px;
border-radius: var(--r-sm);
color: var(--white);
background: var(--brand);
transition: background 0.15s ease, transform 0.12s ease;
}
.promo-copy:hover {
background: var(--brand-d);
}
.promo-copy:active {
transform: translateY(1px);
}
.promo-copy:focus-visible {
outline: none;
box-shadow: 0 0 0 3px var(--bg), 0 0 0 5px var(--brand);
}
.promo-copy.is-copied {
background: var(--accent);
}
.promo-help {
margin: 7px 2px 0;
font-size: 0.8rem;
color: var(--muted);
}
/* Urgency countdown */
.urgency {
display: inline-flex;
align-items: center;
gap: 7px;
margin: 0 0 18px;
font-size: 0.92rem;
color: var(--ink-2);
background: rgba(217, 138, 43, 0.12);
border: 1px solid rgba(217, 138, 43, 0.28);
border-radius: 999px;
padding: 7px 14px;
}
.urgency-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--warn);
box-shadow: 0 0 0 0 rgba(217, 138, 43, 0.6);
animation: pulse 1.6s ease-out infinite;
}
.countdown {
font-variant-numeric: tabular-nums;
font-weight: 800;
color: var(--warn);
letter-spacing: 0.02em;
}
.urgency.is-expired {
background: rgba(212, 80, 62, 0.1);
border-color: rgba(212, 80, 62, 0.3);
}
.urgency.is-expired .urgency-dot {
background: var(--danger);
animation: none;
}
.urgency.is-expired .countdown {
color: var(--danger);
}
.modal-actions {
display: flex;
flex-direction: column;
gap: 10px;
}
.modal-foot {
margin: 16px 0 0;
font-size: 0.78rem;
color: var(--muted);
}
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
z-index: 60;
display: flex;
flex-direction: column;
gap: 8px;
align-items: center;
pointer-events: none;
}
.toast {
display: inline-flex;
align-items: center;
gap: 9px;
background: var(--ink);
color: var(--white);
font-size: 0.9rem;
font-weight: 600;
padding: 11px 16px;
border-radius: 999px;
box-shadow: var(--sh-2);
animation: toast-in 0.22s ease both;
}
.toast.is-out {
animation: toast-out 0.22s ease forwards;
}
.toast svg {
flex: 0 0 auto;
color: var(--accent);
}
/* ---------- Animations ---------- */
@keyframes fade-in {
from {
opacity: 0;
}
}
@keyframes pop-in {
from {
opacity: 0;
transform: translate(-50%, -42%) scale(0.94);
}
}
@keyframes ray-spin {
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
70% {
box-shadow: 0 0 0 7px rgba(217, 138, 43, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(217, 138, 43, 0);
}
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateY(10px);
}
}
@keyframes toast-out {
to {
opacity: 0;
transform: translateY(10px);
}
}
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.appnav .nav-link {
display: none;
}
.shell {
padding: 28px 16px;
}
.modal {
padding: 40px 18px 20px;
}
.modal-title {
font-size: 1.22rem;
}
.price-compare {
flex-direction: column;
gap: 8px;
}
.price-arrow {
transform: rotate(90deg);
}
.price-block {
flex-direction: row;
justify-content: space-between;
text-align: left;
align-items: center;
}
.price-new {
flex-wrap: wrap;
}
.promo-copy-text {
display: none;
}
.promo-copy {
padding: 8px 12px;
}
}
/* ---------- Reduced motion ---------- */
@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";
var overlay = document.getElementById("overlay");
var modal = document.getElementById("modal");
var modalClose = document.getElementById("modalClose");
var claimBtn = document.getElementById("claimBtn");
var declineBtn = document.getElementById("declineBtn");
var promoInput = document.getElementById("promoCode");
var promoCopy = document.getElementById("promoCopy");
var promoField = promoCopy ? promoCopy.closest(".promo-field") : null;
var copyText = promoCopy ? promoCopy.querySelector(".promo-copy-text") : null;
var icCopy = promoCopy ? promoCopy.querySelector(".ic-copy") : null;
var icCheck = promoCopy ? promoCopy.querySelector(".ic-check") : null;
var countdownEl = document.getElementById("countdown");
var urgencyEl = document.getElementById("urgency");
var toastWrap = document.getElementById("toastWrap");
var openTriggers = document.querySelectorAll("[data-open-offer]");
var FOCUSABLE =
'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])';
var isOpen = false;
var lastFocused = null;
var hasOpenedOnce = false;
var countdownTimer = null;
var remaining = 10 * 60; // 10:00
var copyResetTimer = null;
/* ---------------- Toast ---------------- */
var CHECK_SVG =
'<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 5 5 9-11"/></svg>';
function toast(msg, withCheck) {
if (!toastWrap) return;
var el = document.createElement("div");
el.className = "toast";
el.setAttribute("role", "status");
el.innerHTML = (withCheck ? CHECK_SVG : "") + "<span></span>";
el.querySelector("span").textContent = msg;
toastWrap.appendChild(el);
window.setTimeout(function () {
el.classList.add("is-out");
el.addEventListener("animationend", function () {
if (el.parentNode) el.parentNode.removeChild(el);
});
}, 2200);
}
/* ---------------- Countdown ---------------- */
function fmt(total) {
var m = Math.floor(total / 60);
var s = total % 60;
return m + ":" + (s < 10 ? "0" : "") + s;
}
function tick() {
if (remaining <= 0) {
remaining = 0;
if (countdownEl) countdownEl.textContent = "0:00";
if (urgencyEl) {
urgencyEl.classList.add("is-expired");
var label = urgencyEl.querySelector("span:not(.urgency-dot)");
if (label) label.textContent = "This offer has expired";
if (countdownEl) countdownEl.hidden = true;
}
stopCountdown();
return;
}
if (countdownEl) countdownEl.textContent = fmt(remaining);
remaining -= 1;
}
function startCountdown() {
stopCountdown();
tick();
countdownTimer = window.setInterval(tick, 1000);
}
function stopCountdown() {
if (countdownTimer) {
window.clearInterval(countdownTimer);
countdownTimer = null;
}
}
/* ---------------- Focus trap ---------------- */
function focusables() {
return Array.prototype.filter.call(
modal.querySelectorAll(FOCUSABLE),
function (el) {
return el.offsetParent !== null || el === document.activeElement;
}
);
}
function trap(e) {
if (e.key !== "Tab") return;
var nodes = focusables();
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() {
if (isOpen) return;
isOpen = true;
hasOpenedOnce = true;
lastFocused = document.activeElement;
overlay.hidden = false;
modal.hidden = false;
document.body.style.overflow = "hidden";
remaining = 10 * 60;
if (urgencyEl) urgencyEl.classList.remove("is-expired");
if (countdownEl) countdownEl.hidden = false;
startCountdown();
// focus the decline action first (least destructive default)
window.requestAnimationFrame(function () {
if (declineBtn) declineBtn.focus();
});
document.addEventListener("keydown", onKeydown, true);
}
function closeModal() {
if (!isOpen) return;
isOpen = false;
overlay.hidden = true;
modal.hidden = true;
document.body.style.overflow = "";
stopCountdown();
document.removeEventListener("keydown", onKeydown, true);
if (lastFocused && typeof lastFocused.focus === "function") {
lastFocused.focus();
}
}
function onKeydown(e) {
if (e.key === "Escape") {
e.preventDefault();
closeModal();
toast("Offer dismissed — it'll be in your inbox.");
return;
}
trap(e);
}
/* ---------------- Copy to clipboard ---------------- */
function markCopied() {
if (icCopy) icCopy.hidden = true;
if (icCheck) icCheck.hidden = false;
if (copyText) copyText.textContent = "Copied";
if (promoCopy) promoCopy.classList.add("is-copied");
if (promoField) promoField.classList.add("is-copied");
if (copyResetTimer) window.clearTimeout(copyResetTimer);
copyResetTimer = window.setTimeout(function () {
if (icCopy) icCopy.hidden = false;
if (icCheck) icCheck.hidden = true;
if (copyText) copyText.textContent = "Copy";
if (promoCopy) promoCopy.classList.remove("is-copied");
if (promoField) promoField.classList.remove("is-copied");
}, 2000);
}
function copyCode() {
var code = promoInput ? promoInput.value : "";
function done() {
markCopied();
toast("Code copied: " + code, true);
}
function fallback() {
if (promoInput) {
promoInput.removeAttribute("readonly");
promoInput.focus();
promoInput.select();
try {
document.execCommand("copy");
done();
} catch (err) {
toast("Copy not supported — select the code manually.");
}
promoInput.setAttribute("readonly", "");
}
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(code).then(done, fallback);
} else {
fallback();
}
}
/* ---------------- Wire up ---------------- */
Array.prototype.forEach.call(openTriggers, function (btn) {
btn.addEventListener("click", function (e) {
e.preventDefault();
openModal();
});
});
if (modalClose) modalClose.addEventListener("click", closeModal);
if (overlay)
overlay.addEventListener("click", function () {
closeModal();
toast("Offer dismissed — it'll be in your inbox.");
});
if (declineBtn)
declineBtn.addEventListener("click", function () {
closeModal();
toast("No problem — the discount is saved for 24 hours.");
});
if (claimBtn)
claimBtn.addEventListener("click", function () {
var code = promoInput ? promoInput.value : "";
closeModal();
toast("Applied " + code + " — 30% off your first 3 months!", true);
});
if (promoCopy) promoCopy.addEventListener("click", copyCode);
if (promoInput)
promoInput.addEventListener("focus", function () {
promoInput.select();
});
/* ---------------- Exit-intent simulation ---------------- */
// Fire once when the cursor leaves through the top of the viewport.
function onMouseOut(e) {
if (hasOpenedOnce || isOpen) return;
if (e.clientY <= 0 && !e.relatedTarget && !e.toElement) {
openModal();
toast("Heads up — we saved you a discount.");
}
}
document.addEventListener("mouseout", onMouseOut);
// Demo fallback: auto-trigger after a short delay if exit intent never fires
// (e.g. inside an iframe or on touch devices).
window.setTimeout(function () {
if (!hasOpenedOnce) openModal();
}, 6000);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Northwind — Win-back / Discount Offer Modal</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">
<!-- App shell behind the modal (gives the offer a believable context) -->
<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="M3 12a9 9 0 1 0 18 0 9 9 0 0 0-18 0Z" />
<path d="m8 12 3 3 5-6" />
</svg>
</span>
<span class="brand-name">Northwind <em>Cloud</em></span>
</div>
<nav class="appnav" aria-label="Primary">
<a href="#" class="nav-link">Product</a>
<a href="#" class="nav-link">Pricing</a>
<a href="#" class="nav-link">Docs</a>
<button class="btn btn-ghost" type="button" data-open-offer>Sign out</button>
</nav>
</header>
<main class="shell" id="shell">
<section class="hero">
<p class="eyebrow">Checkout</p>
<h1 class="hero-title">You're one step from going Pro</h1>
<p class="hero-sub">Review your plan below. Your card won't be charged until you confirm —
and you can change or cancel anytime from billing settings.</p>
<div class="summary" role="group" aria-label="Order summary">
<div class="summary-row">
<span class="summary-label">Northwind Pro</span>
<span class="summary-val">$24<span class="per">/mo</span></span>
</div>
<div class="summary-row">
<span class="summary-label">Billing</span>
<span class="summary-val muted">Monthly · 14-day free trial</span>
</div>
<div class="summary-row summary-total">
<span class="summary-label">Due today</span>
<span class="summary-val">$0.00</span>
</div>
<button class="btn btn-primary btn-lg btn-block" type="button" data-open-offer>Start free trial</button>
<p class="summary-foot">By continuing you agree to our (fictional) Terms.</p>
</div>
<p class="hint">This demo simulates an exit-intent moment. <button class="btn btn-link" type="button" data-open-offer>Trigger the win-back offer</button> — or just move your cursor toward the top of the window.</p>
</section>
</main>
<!-- Win-back / discount offer modal -->
<div class="overlay" id="overlay" hidden></div>
<div class="modal" id="modal" role="dialog" aria-modal="true" aria-labelledby="offerTitle" aria-describedby="offerSub" hidden>
<div class="modal-glow" aria-hidden="true"></div>
<button class="icon-btn modal-close" id="modalClose" type="button" aria-label="Close offer">
<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 class="badge-burst" aria-hidden="true">
<svg viewBox="0 0 120 120" width="92" height="92" class="burst-rays">
<g class="ray-group" stroke="currentColor" stroke-width="3" stroke-linecap="round">
<line x1="60" y1="6" x2="60" y2="20" />
<line x1="60" y1="100" x2="60" y2="114" />
<line x1="6" y1="60" x2="20" y2="60" />
<line x1="100" y1="60" x2="114" y2="60" />
<line x1="22" y1="22" x2="32" y2="32" />
<line x1="88" y1="88" x2="98" y2="98" />
<line x1="98" y1="22" x2="88" y2="32" />
<line x1="32" y1="88" x2="22" y2="98" />
</g>
</svg>
<span class="badge-disc">
<strong class="disc-num">30%</strong>
<span class="disc-word">OFF</span>
</span>
</div>
<p class="modal-eyebrow">Before you go</p>
<h2 class="modal-title" id="offerTitle">Wait — here's 30% off your first 3 months</h2>
<p class="modal-sub" id="offerSub">We'd love to keep you on Northwind Pro. Lock in the lower rate now —
it sticks for three full billing cycles, then returns to standard pricing.</p>
<!-- Old vs new price -->
<div class="price-compare" role="group" aria-label="Discounted price">
<div class="price-block price-old">
<span class="price-cap">Standard</span>
<span class="price-amt"><s>$24</s><span class="price-per">/mo</span></span>
</div>
<span class="price-arrow" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
</span>
<div class="price-block price-new">
<span class="price-cap">Your price · 3 mo</span>
<span class="price-amt">$16.80<span class="price-per">/mo</span></span>
<span class="price-save" aria-label="Save 7 dollars 20 cents per month">Save $7.20/mo</span>
</div>
</div>
<!-- Copyable promo code -->
<div class="promo">
<label class="promo-label" id="promoLabel">Your promo code</label>
<div class="promo-field" role="group" aria-labelledby="promoLabel">
<input class="promo-input" id="promoCode" type="text" value="STAYWITHUS30" readonly aria-readonly="true" aria-label="Promo code STAYWITHUS30" />
<button class="promo-copy" id="promoCopy" type="button" aria-label="Copy promo code STAYWITHUS30">
<svg class="ic-copy" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<rect x="9" y="9" width="11" height="11" rx="2" />
<path d="M5 15V5a2 2 0 0 1 2-2h10" />
</svg>
<svg class="ic-check" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" hidden>
<path d="m5 12 5 5 9-11" />
</svg>
<span class="promo-copy-text">Copy</span>
</button>
</div>
<p class="promo-help">Applied automatically at checkout when you claim the offer.</p>
</div>
<!-- Urgency countdown -->
<p class="urgency" id="urgency">
<span class="urgency-dot" aria-hidden="true"></span>
<span>Offer expires in</span>
<strong class="countdown" id="countdown" aria-live="polite" aria-atomic="true">10:00</strong>
</p>
<div class="modal-actions">
<button class="btn btn-primary btn-lg btn-block" id="claimBtn" type="button">Claim 30% off now</button>
<button class="btn btn-ghost btn-block" id="declineBtn" type="button">No thanks, maybe later</button>
</div>
<p class="modal-foot">One-time offer · applies to the next 3 billing cycles · 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>Win-back / discount offer modal
A complete exit-intent retention pattern for the fictional Northwind Cloud, art-directed in the
neutral product palette with an indigo brand and a celebratory teal accent. Behind the offer sits a
believable checkout shell — an order summary for Northwind Pro at $24/mo with a 14-day trial — so the
interruption reads like a real abandonment moment. When the cursor leaves through the top of the
viewport (or you tap any trigger), a centered role="dialog" modal pops in with a spinning SVG
starburst around a circular 30% OFF badge. No confetti, just a confident accent.
The body pairs the old and new prices side by side: a struck-through $24/mo standard rate next to the
discounted $16.80/mo with a “Save $7.20/mo” pill. A dashed promo-code field holds STAYWITHUS30 with
a one-tap copy button that swaps its icon to a checkmark, flips to “Copied”, glows the field, and
fires a toast — backed by a document.execCommand fallback for older or sandboxed environments. An
amber urgency chip counts down from 10:00 in tabular numerals via an aria-live region, then flips to
an expired state when it hits zero. “Claim 30% off now” and a quieter “No thanks, maybe later” close
the dialog with contextual toasts.
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. The dialog uses aria-modal, a Tab focus trap, Esc and
overlay-click to dismiss, and focuses the least-destructive action on open. A
prefers-reduced-motion block neutralizes the pop, spin, and pulse animations, and below 520px the
price comparison stacks vertically and the copy button collapses to its icon.
Illustrative UI only — the brand, prices, and promo code are fictional; not a real offer.