Nonprofit — Donation Flow
A warm, four-step nonprofit donation flow for the fictional Open Harvest charity. Pick a preset or custom amount, toggle one-time versus monthly giving, choose a designation, and optionally cover processing fees. Live impact equivalence translates each gift into meals served, while a fundraising thermometer, impact stats, and a donor wall build trust. Inline validation guides donor details and payment before a review step and an animated thank-you screen confirm the gift.
MCP
Code
:root {
--brand: #1f7a6d;
--brand-d: #155e54;
--accent: #e8743b;
--accent-d: #cc5d28;
--ink: #2a2722;
--ink-2: #524d44;
--muted: #7a7368;
--bg: #faf6f0;
--surface: #ffffff;
--line: rgba(42, 39, 34, 0.1);
--line-2: rgba(42, 39, 34, 0.18);
--ok: #2f9e6f;
--warn: #d98a2b;
--danger: #d4503e;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 22px;
--sh-sm: 0 1px 2px rgba(42, 39, 34, 0.06), 0 2px 8px rgba(42, 39, 34, 0.05);
--sh-md: 0 8px 30px rgba(42, 39, 34, 0.1);
--sh-lg: 0 18px 50px rgba(42, 39, 34, 0.14);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: var(--ink);
background: var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 { font-family: "Fraunces", Georgia, serif; line-height: 1.18; margin: 0; }
button { font-family: inherit; cursor: pointer; }
.page { max-width: 1120px; margin: 0 auto; padding: 22px 20px 60px; }
/* header */
.site-head {
display: flex; align-items: center; justify-content: space-between;
gap: 16px; flex-wrap: wrap; margin-bottom: 26px;
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
display: grid; place-items: center; width: 38px; height: 38px;
border-radius: 50%; color: #fff; font-size: 22px;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
box-shadow: var(--sh-sm);
}
.brand-name { font-family: "Fraunces", serif; font-weight: 700; font-size: 21px; letter-spacing: -0.01em; }
.head-trust { display: flex; gap: 8px; flex-wrap: wrap; }
.badge {
display: inline-flex; align-items: center; gap: 6px;
font-size: 12.5px; font-weight: 600; color: var(--brand-d);
background: rgba(31, 122, 109, 0.1); border: 1px solid rgba(31, 122, 109, 0.2);
padding: 5px 11px; border-radius: 999px;
}
.badge-soft { color: var(--ink-2); background: var(--surface); border-color: var(--line); }
/* layout */
.layout {
display: grid; grid-template-columns: 1.25fr 0.85fr; gap: 26px; align-items: start;
}
/* give card */
.give {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--r-lg); padding: 26px 26px 28px; box-shadow: var(--sh-md);
}
.steps { display: flex; gap: 6px; list-style: none; margin: 0 0 24px; padding: 0; flex-wrap: wrap; }
.step {
display: flex; align-items: center; gap: 7px; font-size: 13px; font-weight: 600;
color: var(--muted); padding: 5px 9px 5px 5px; border-radius: 999px; transition: color 0.2s;
}
.step span {
display: grid; place-items: center; width: 22px; height: 22px; border-radius: 50%;
background: var(--bg); border: 1px solid var(--line-2); font-size: 12px; transition: all 0.2s;
}
.step.is-active { color: var(--ink); }
.step.is-active span { background: var(--brand); border-color: var(--brand); color: #fff; }
.step.is-done span { background: var(--ok); border-color: var(--ok); color: #fff; }
.panel { border: 0; padding: 0; margin: 0; min-inline-size: 0; }
.panel.is-hidden { display: none; }
.give-title { font-size: 27px; font-weight: 600; letter-spacing: -0.01em; }
.give-sub { color: var(--ink-2); margin: 8px 0 20px; }
.panel-h { font-size: 22px; font-weight: 600; }
.panel-p { color: var(--ink-2); margin: 7px 0 18px; font-size: 14.5px; }
/* frequency */
.freq {
display: grid; grid-template-columns: 1fr 1fr; gap: 8px; padding: 5px;
background: var(--bg); border: 1px solid var(--line); border-radius: 999px; margin-bottom: 18px;
}
.freq-btn {
border: 0; background: transparent; border-radius: 999px; padding: 11px;
font-size: 14.5px; font-weight: 600; color: var(--ink-2); transition: all 0.18s;
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
}
.freq-btn.is-on { background: var(--surface); color: var(--ink); box-shadow: var(--sh-sm); }
.freq-tag {
font-size: 11px; font-weight: 700; color: var(--accent-d);
background: rgba(232, 116, 59, 0.14); padding: 2px 7px; border-radius: 999px;
}
/* amounts */
.amounts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 18px; }
.amt {
border: 1.5px solid var(--line-2); background: var(--surface); border-radius: var(--r-md);
padding: 14px 8px; font-size: 17px; font-weight: 700; color: var(--ink);
transition: all 0.16s; position: relative;
}
.amt:hover { border-color: var(--brand); transform: translateY(-1px); }
.amt:active { transform: translateY(0); }
.amt.is-on {
border-color: var(--brand); background: rgba(31, 122, 109, 0.07);
box-shadow: 0 0 0 3px rgba(31, 122, 109, 0.13);
}
.amt-custom { display: flex; align-items: center; gap: 2px; padding: 0 12px; cursor: text; }
.amt-cur { color: var(--muted); font-weight: 700; }
.amt-custom input {
border: 0; outline: 0; background: transparent; width: 100%; font: inherit;
font-weight: 700; font-size: 16px; color: var(--ink); padding: 14px 0;
}
/* impact banner */
.impact {
display: flex; align-items: center; gap: 14px; margin: 0 0 20px;
background: linear-gradient(135deg, rgba(31, 122, 109, 0.08), rgba(232, 116, 59, 0.08));
border: 1px solid var(--line); border-radius: var(--r-md); padding: 14px 16px;
}
.impact-emoji { font-size: 30px; line-height: 1; transition: transform 0.3s; }
.impact-emoji.pop { transform: scale(1.25) rotate(-6deg); }
.impact-text { display: flex; flex-direction: column; font-size: 14.5px; line-height: 1.45; }
.impact-text strong { color: var(--brand-d); font-size: 15.5px; }
.impact-text span { color: var(--ink-2); }
/* fields */
.field { display: block; margin-bottom: 16px; }
.field-label { display: block; font-size: 13px; font-weight: 600; color: var(--ink-2); margin-bottom: 6px; }
.input, .select {
width: 100%; font: inherit; font-size: 15px; color: var(--ink);
background: var(--surface); border: 1.5px solid var(--line-2); border-radius: var(--r-sm);
padding: 11px 13px; transition: border-color 0.16s, box-shadow 0.16s;
}
.input:focus, .select:focus, .amt-custom:focus-within {
outline: 0; border-color: var(--brand); box-shadow: 0 0 0 3px rgba(31, 122, 109, 0.15);
}
.input.invalid { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(212, 80, 62, 0.13); }
.err { display: block; color: var(--danger); font-size: 12.5px; font-weight: 600; margin-top: 5px; min-height: 0; }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
/* cover fees / checkboxes */
.cover {
display: flex; gap: 11px; align-items: flex-start; font-size: 14px; color: var(--ink-2);
background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-md);
padding: 13px 15px; margin-bottom: 22px; cursor: pointer; line-height: 1.45;
}
.cover input { margin-top: 3px; width: 17px; height: 17px; accent-color: var(--brand); cursor: pointer; flex: none; }
.cover strong { color: var(--ink); }
/* CTAs */
.cta {
width: 100%; border: 0; border-radius: var(--r-md); padding: 15px;
font-size: 16px; font-weight: 700; color: #fff;
background: linear-gradient(135deg, var(--accent), var(--accent-d));
box-shadow: 0 8px 20px rgba(232, 116, 59, 0.3); transition: transform 0.14s, box-shadow 0.14s;
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
}
.cta:hover { transform: translateY(-2px); box-shadow: 0 12px 26px rgba(232, 116, 59, 0.38); }
.cta:active { transform: translateY(0); }
.cta:disabled { opacity: 0.65; cursor: progress; transform: none; }
.cta-give { font-size: 17px; }
.ghost {
border: 1.5px solid var(--line-2); background: var(--surface); color: var(--ink-2);
border-radius: var(--r-md); padding: 14px 20px; font-size: 15px; font-weight: 600;
transition: all 0.16s;
}
.ghost:hover { border-color: var(--brand); color: var(--brand-d); }
.nav-row { display: flex; gap: 12px; margin-top: 6px; }
.nav-row .cta { width: auto; flex: 1; }
/* review */
.review { list-style: none; margin: 0 0 16px; padding: 0; border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
.review li {
display: flex; justify-content: space-between; gap: 14px;
padding: 12px 16px; font-size: 14.5px; border-bottom: 1px solid var(--line);
}
.review li:last-child { border-bottom: 0; }
.review li span { color: var(--ink-2); }
.review li b { color: var(--ink); font-weight: 600; text-align: right; }
.review li.total { background: var(--bg); }
.review li.total span, .review li.total b { font-size: 16px; font-weight: 700; color: var(--ink); }
.fineprint { font-size: 12.5px; color: var(--muted); margin: 0 0 16px; }
/* success */
.success { text-align: center; padding: 14px 4px 8px; }
.success-burst { font-size: 52px; animation: pop 0.5s ease; }
@keyframes pop { 0% { transform: scale(0.4); opacity: 0; } 60% { transform: scale(1.18); } 100% { transform: scale(1); opacity: 1; } }
.success-h { font-size: 26px; font-weight: 600; margin: 8px 0 4px; }
.success-amt { font-size: 19px; font-weight: 700; color: var(--brand-d); margin: 0 0 14px; }
.success-impact {
display: inline-block; font-size: 15px; font-weight: 600; color: var(--accent-d);
background: rgba(232, 116, 59, 0.1); border: 1px solid rgba(232, 116, 59, 0.22);
border-radius: 999px; padding: 9px 18px; margin-bottom: 16px;
}
.success-note { color: var(--ink-2); font-size: 14.5px; margin: 0 0 20px; }
/* rail */
.rail { display: flex; flex-direction: column; gap: 18px; position: sticky; top: 18px; }
.photo {
height: 180px; border-radius: var(--r-lg); position: relative; overflow: hidden;
background:
radial-gradient(120% 90% at 15% 10%, rgba(232, 116, 59, 0.55), transparent 55%),
linear-gradient(135deg, var(--brand), var(--brand-d));
box-shadow: var(--sh-sm);
}
.photo::after {
content: ""; position: absolute; inset: 0;
background: radial-gradient(70% 60% at 80% 80%, rgba(255, 255, 255, 0.18), transparent 60%);
}
.photo-cap {
position: absolute; left: 14px; right: 14px; bottom: 12px; z-index: 1;
color: #fff; font-size: 12.5px; font-weight: 600; text-shadow: 0 1px 4px rgba(0, 0, 0, 0.35);
}
.thermo, .donors {
background: var(--surface); border: 1px solid var(--line);
border-radius: var(--r-md); padding: 16px 18px; box-shadow: var(--sh-sm);
}
.thermo-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 10px; }
.thermo-head strong { font-size: 15px; }
.thermo-head span { font-weight: 700; color: var(--accent-d); }
.bar { height: 12px; background: var(--bg); border: 1px solid var(--line); border-radius: 999px; overflow: hidden; }
.bar-fill {
height: 100%; border-radius: 999px;
background: linear-gradient(90deg, var(--accent), var(--accent-d));
transition: width 0.8s cubic-bezier(0.22, 1, 0.36, 1);
}
.thermo-foot { display: flex; justify-content: space-between; margin-top: 9px; font-size: 13px; color: var(--ink-2); }
.thermo-foot strong { color: var(--ink); }
.stats { list-style: none; display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 0; padding: 0; }
.stats li {
background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-md);
padding: 12px 8px; text-align: center; box-shadow: var(--sh-sm);
}
.stats strong { display: block; font-family: "Fraunces", serif; font-size: 20px; color: var(--brand-d); }
.stats span { font-size: 11.5px; color: var(--muted); line-height: 1.3; display: block; margin-top: 2px; }
.donors h3 { font-size: 14px; font-weight: 700; margin-bottom: 10px; }
.donors ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 9px; }
.donors li { font-size: 13.5px; color: var(--ink-2); display: flex; align-items: center; gap: 8px; }
.donors li b { color: var(--ink); }
.dot { width: 8px; height: 8px; border-radius: 50%; background: var(--ok); flex: none; box-shadow: 0 0 0 3px rgba(47, 158, 111, 0.16); }
.seal { text-align: center; font-size: 12.5px; color: var(--muted); font-weight: 600; margin: 0; }
.seal span { color: var(--warn); }
/* toast */
.toast {
position: fixed; left: 50%; bottom: 26px; transform: translate(-50%, 20px);
background: var(--ink); color: #fff; padding: 12px 20px; border-radius: 999px;
font-size: 14px; font-weight: 600; box-shadow: var(--sh-lg);
opacity: 0; pointer-events: none; transition: opacity 0.25s, transform 0.25s; z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* responsive */
@media (max-width: 900px) {
.layout { grid-template-columns: 1fr; }
.rail { position: static; }
.stats { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 520px) {
.page { padding: 16px 14px 48px; }
.give { padding: 20px 17px 22px; }
.give-title { font-size: 23px; }
.amounts { grid-template-columns: repeat(2, 1fr); }
.grid-2 { grid-template-columns: 1fr; }
.steps .step { font-size: 0; gap: 0; padding: 3px; }
.steps .step span { font-size: 12px; }
.nav-row { flex-direction: column-reverse; }
.nav-row .ghost { width: 100%; }
}(function () {
"use strict";
// ---- state ----
var state = {
amount: 50,
freq: "once",
designation: "Where needed most",
coverFees: true,
anon: false,
fname: "",
lname: "",
email: "",
step: 1,
};
var FEE_RATE = 0.029;
var FEE_FLAT = 0.3;
// ---- helpers ----
var $ = function (sel, root) { return (root || document).querySelector(sel); };
var $$ = function (sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); };
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { toastEl.classList.remove("show"); }, 2600);
}
function fee() {
return state.amount > 0 ? state.amount * FEE_RATE + FEE_FLAT : 0;
}
function total() {
return state.amount + (state.coverFees ? fee() : 0);
}
function money(n) {
return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
// impact equivalence — meals at 38¢ each, plus a flavor line
function impact() {
var meals = Math.max(1, Math.round(state.amount / 2.5));
var line, emoji, when;
if (state.amount >= 250) {
emoji = "🚛";
line = "Stocks an entire pantry shelf";
when = "and delivers " + meals + " meals across the county.";
} else if (state.amount >= 100) {
emoji = "📦";
line = "Fills " + Math.round(state.amount / 25) + " emergency food boxes";
when = "— about " + meals + " meals for local families.";
} else if (state.amount >= 50) {
emoji = "🍲";
line = "Provides " + meals + " hot meals";
when = "delivered this week to a local shelter.";
} else {
emoji = "🥪";
line = "Serves " + meals + " school lunches";
when = "for kids who'd otherwise go hungry.";
}
if (state.freq === "monthly") {
when = when + " Every month.";
}
return { meals: meals, line: line, emoji: emoji, when: when };
}
// ---- renderers ----
function renderImpact(animate) {
var i = impact();
$("#impactLine").textContent = i.line;
$("#impactWhen").textContent = i.when;
var em = $("#impactEmoji");
em.textContent = i.emoji;
if (animate) {
em.classList.remove("pop");
void em.offsetWidth;
em.classList.add("pop");
}
}
function renderFee() {
$("#feeAmt").textContent = money(fee()).replace(/\.00$/, ".00");
}
function renderSubmitAmt() {
var label = money(total()) + (state.freq === "monthly" ? "/mo" : "");
$("#submitAmt").textContent = label;
}
function renderAll(animate) {
renderImpact(animate);
renderFee();
renderSubmitAmt();
}
// ---- amount selection ----
var customInput = $("#customAmt");
function setAmount(val, fromCustom) {
state.amount = val;
$$(".amt[data-amt]").forEach(function (b) {
var on = parseInt(b.getAttribute("data-amt"), 10) === val && !fromCustom;
b.classList.toggle("is-on", on);
b.setAttribute("aria-checked", on ? "true" : "false");
});
if (!fromCustom) customInput.value = "";
renderAll(true);
}
$$(".amt[data-amt]").forEach(function (b) {
b.addEventListener("click", function () {
setAmount(parseInt(b.getAttribute("data-amt"), 10), false);
});
});
customInput.addEventListener("input", function () {
var v = parseInt(customInput.value, 10);
$$(".amt[data-amt]").forEach(function (b) {
b.classList.remove("is-on");
b.setAttribute("aria-checked", "false");
});
state.amount = isNaN(v) || v < 1 ? 0 : v;
renderAll(true);
});
// ---- frequency ----
$$(".freq-btn").forEach(function (b) {
b.addEventListener("click", function () {
state.freq = b.getAttribute("data-freq");
$$(".freq-btn").forEach(function (x) {
var on = x === b;
x.classList.toggle("is-on", on);
x.setAttribute("aria-checked", on ? "true" : "false");
});
renderAll(true);
});
});
// ---- designation / cover / anon ----
$("#designation").addEventListener("change", function (e) { state.designation = e.target.value; });
$("#coverFees").addEventListener("change", function (e) {
state.coverFees = e.target.checked;
renderSubmitAmt();
});
$("#anon").addEventListener("change", function (e) { state.anon = e.target.checked; });
// ---- step navigation ----
var panels = {};
$$(".panel[data-step]").forEach(function (p) { panels[p.getAttribute("data-step")] = p; });
function showStep(step) {
state.step = step;
Object.keys(panels).forEach(function (k) {
panels[k].classList.toggle("is-hidden", k !== String(step));
});
$$(".step[data-step-dot]").forEach(function (dot) {
var n = parseInt(dot.getAttribute("data-step-dot"), 10);
dot.classList.toggle("is-active", n === step);
dot.classList.toggle("is-done", n < step && step !== "done");
});
if (step === "done") {
$$(".step[data-step-dot]").forEach(function (dot) { dot.classList.add("is-done"); dot.classList.remove("is-active"); });
}
var card = $(".give");
if (card && card.scrollIntoView) card.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
// ---- validation ----
function setError(input, msg) {
var holder = input.closest(".field");
var err = holder ? $("[data-err]", holder) : null;
if (err) err.textContent = msg || "";
input.classList.toggle("invalid", !!msg);
}
function validateStep1() {
if (state.amount < 1) { toast("Please choose a donation amount."); customInput.focus(); return false; }
return true;
}
function validateStep2() {
var ok = true;
var f = $("#fname"), l = $("#lname"), e = $("#email");
[f, l, e].forEach(function (i) { setError(i, ""); });
if (!f.value.trim()) { setError(f, "Required"); ok = false; }
if (!l.value.trim()) { setError(l, "Required"); ok = false; }
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(e.value.trim())) { setError(e, "Enter a valid email"); ok = false; }
if (ok) { state.fname = f.value.trim(); state.lname = l.value.trim(); state.email = e.value.trim(); }
return ok;
}
function validateStep3() {
var ok = true;
var c = $("#card"), x = $("#exp"), v = $("#cvc");
[c, x, v].forEach(function (i) { setError(i, ""); });
var digits = c.value.replace(/\s/g, "");
if (!/^\d{15,16}$/.test(digits)) { setError(c, "Enter a 15–16 digit card number"); ok = false; }
if (!/^(0[1-9]|1[0-2])\/\d{2}$/.test(x.value.trim())) { setError(x, "Use MM/YY"); ok = false; }
if (!/^\d{3,4}$/.test(v.value.trim())) { setError(v, "3–4 digits"); ok = false; }
return ok;
}
// ---- review ----
function buildReview() {
var i = impact();
var rows = [
["Gift amount", money(state.amount) + (state.freq === "monthly" ? " / month" : " one-time")],
["Goes to", state.designation],
["Processing fee", state.coverFees ? money(fee()) + " (covered)" : "Not covered"],
["Donor", state.anon ? "Anonymous" : (state.fname + " " + state.lname)],
["Receipt to", state.email],
["Your impact", i.line.toLowerCase()],
];
var html = rows.map(function (r) {
return "<li><span>" + r[0] + "</span><b>" + escapeHtml(r[1]) + "</b></li>";
}).join("");
html += "<li class='total'><span>Total " + (state.freq === "monthly" ? "monthly" : "today") +
"</span><b>" + money(total()) + "</b></li>";
$("#reviewList").innerHTML = html;
}
function escapeHtml(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
// ---- next / back wiring ----
$$("[data-next]").forEach(function (btn) {
btn.addEventListener("click", function () {
var cur = state.step;
if (cur === 1 && !validateStep1()) return;
if (cur === 2 && !validateStep2()) return;
if (cur === 3 && !validateStep3()) return;
if (cur === 3) buildReview();
showStep(cur + 1);
});
});
$$("[data-back]").forEach(function (btn) {
btn.addEventListener("click", function () { showStep(state.step - 1); });
});
// ---- submit ----
$("#donateForm").addEventListener("submit", function (e) {
e.preventDefault();
var btn = $("#submitBtn");
btn.disabled = true;
btn.textContent = "Processing…";
setTimeout(function () {
var i = impact();
$("#thankName").textContent = state.anon ? "friend" : state.fname;
$("#successAmt").textContent = "You gave " + money(total()) + (state.freq === "monthly" ? " every month" : "");
$("#successImpact").textContent = "That's " + i.line.charAt(0).toLowerCase() + i.line.slice(1) + ".";
showStep("done");
bumpThermo();
addDonor();
btn.disabled = false;
btn.innerHTML = "Give <span id='submitAmt'>" + money(total()) + "</span>";
toast("Gift confirmed — thank you! 💚");
}, 1100);
});
// ---- restart ----
$("#restartBtn").addEventListener("click", function () {
showStep(1);
setAmount(50, false);
state.freq = "once";
$$(".freq-btn").forEach(function (x) {
var on = x.getAttribute("data-freq") === "once";
x.classList.toggle("is-on", on);
x.setAttribute("aria-checked", on ? "true" : "false");
});
["fname", "lname", "email", "card", "exp", "cvc"].forEach(function (id) {
var el = $("#" + id); if (el) { el.value = ""; el.classList.remove("invalid"); }
});
$$("[data-err]").forEach(function (e) { e.textContent = ""; });
});
// ---- thermometer bump on donation ----
function bumpThermo() {
var fill = $("#barFill");
var pctEl = $("#goalPct");
var cur = parseFloat(fill.style.width) || 73;
var next = Math.min(99, cur + Math.max(0.3, state.amount / 4000 * 100));
fill.style.width = next.toFixed(1) + "%";
pctEl.textContent = Math.round(next) + "%";
var bar = fill.parentElement;
bar.setAttribute("aria-valuenow", Math.round(next));
}
function addDonor() {
var list = $("#donorList");
var name = state.anon ? "Anonymous" : (state.fname + " " + (state.lname ? state.lname.charAt(0) + "." : ""));
var li = document.createElement("li");
li.innerHTML = "<span class='dot'></span> " + escapeHtml(name) + " gave <b>$" +
state.amount + "</b>" + (state.freq === "monthly" ? " · monthly" : "");
li.style.opacity = "0";
list.insertBefore(li, list.firstChild);
requestAnimationFrame(function () {
li.style.transition = "opacity 0.5s";
li.style.opacity = "1";
});
while (list.children.length > 5) list.removeChild(list.lastChild);
}
// ---- init ----
renderAll(false);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Open Harvest — Give Today</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=Fraunces:opsz,[email protected],500;9..144,600;9..144,700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="site-head">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◐</span>
<span class="brand-name">Open Harvest</span>
</div>
<div class="head-trust">
<span class="badge"><span aria-hidden="true">✓</span> Registered Charity #84-2910</span>
<span class="badge badge-soft"><span aria-hidden="true">🔒</span> Tax-deductible</span>
</div>
</header>
<main class="layout">
<!-- LEFT: form / flow -->
<section class="give" aria-label="Donation flow">
<ol class="steps" aria-label="Progress">
<li class="step is-active" data-step-dot="1"><span>1</span> Gift</li>
<li class="step" data-step-dot="2"><span>2</span> Details</li>
<li class="step" data-step-dot="3"><span>3</span> Payment</li>
<li class="step" data-step-dot="4"><span>4</span> Review</li>
</ol>
<form id="donateForm" novalidate>
<!-- STEP 1 -->
<fieldset class="panel" data-step="1">
<h1 class="give-title">Your gift feeds a family today</h1>
<p class="give-sub">Every dollar is matched 1:1 through June. Choose an amount to see your impact.</p>
<div class="freq" role="radiogroup" aria-label="Giving frequency">
<button type="button" class="freq-btn is-on" data-freq="once" role="radio" aria-checked="true">One-time</button>
<button type="button" class="freq-btn" data-freq="monthly" role="radio" aria-checked="false">
Monthly <span class="freq-tag">most impact</span>
</button>
</div>
<div class="amounts" role="radiogroup" aria-label="Donation amount">
<button type="button" class="amt" data-amt="25" role="radio" aria-checked="false">$25</button>
<button type="button" class="amt is-on" data-amt="50" role="radio" aria-checked="true">$50</button>
<button type="button" class="amt" data-amt="100" role="radio" aria-checked="false">$100</button>
<button type="button" class="amt" data-amt="250" role="radio" aria-checked="false">$250</button>
<button type="button" class="amt" data-amt="500" role="radio" aria-checked="false">$500</button>
<label class="amt amt-custom">
<span class="amt-cur">$</span>
<input id="customAmt" type="number" min="1" step="1" inputmode="numeric" placeholder="Other" aria-label="Custom amount" />
</label>
</div>
<div class="impact" aria-live="polite">
<div class="impact-emoji" id="impactEmoji" aria-hidden="true">🍲</div>
<div class="impact-text">
<strong id="impactLine">Provides 20 hot meals</strong>
<span id="impactWhen">delivered this week to a local shelter.</span>
</div>
</div>
<label class="field" for="designation">
<span class="field-label">Direct my gift to</span>
<select id="designation" class="select">
<option value="Where needed most">Where needed most</option>
<option value="Emergency food boxes">Emergency food boxes</option>
<option value="School lunch program">School lunch program</option>
<option value="Community gardens">Community gardens</option>
<option value="Disaster relief">Disaster relief</option>
</select>
</label>
<label class="cover" for="coverFees">
<input type="checkbox" id="coverFees" checked />
<span>
<strong>Cover the <span id="feeAmt">$1.85</span> processing fee</strong>
so 100% of my gift reaches the mission.
</span>
</label>
<button type="button" class="cta" data-next>Continue <span aria-hidden="true">→</span></button>
</fieldset>
<!-- STEP 2 -->
<fieldset class="panel is-hidden" data-step="2">
<h2 class="panel-h">Your details</h2>
<p class="panel-p">We'll send your receipt and impact updates here.</p>
<div class="grid-2">
<label class="field" for="fname">
<span class="field-label">First name</span>
<input id="fname" class="input" type="text" autocomplete="given-name" required />
<span class="err" data-err></span>
</label>
<label class="field" for="lname">
<span class="field-label">Last name</span>
<input id="lname" class="input" type="text" autocomplete="family-name" required />
<span class="err" data-err></span>
</label>
</div>
<label class="field" for="email">
<span class="field-label">Email</span>
<input id="email" class="input" type="email" autocomplete="email" required />
<span class="err" data-err></span>
</label>
<label class="cover" for="anon">
<input type="checkbox" id="anon" />
<span>Make my gift anonymous on the donor wall.</span>
</label>
<div class="nav-row">
<button type="button" class="ghost" data-back>← Back</button>
<button type="button" class="cta" data-next>Continue <span aria-hidden="true">→</span></button>
</div>
</fieldset>
<!-- STEP 3 -->
<fieldset class="panel is-hidden" data-step="3">
<h2 class="panel-h">Payment</h2>
<p class="panel-p"><span aria-hidden="true">🔒</span> Encrypted & secure. This is a demo — no real charge.</p>
<label class="field" for="card">
<span class="field-label">Card number</span>
<input id="card" class="input" type="text" inputmode="numeric" placeholder="4242 4242 4242 4242" autocomplete="cc-number" required />
<span class="err" data-err></span>
</label>
<div class="grid-2">
<label class="field" for="exp">
<span class="field-label">Expiry</span>
<input id="exp" class="input" type="text" placeholder="MM/YY" autocomplete="cc-exp" required />
<span class="err" data-err></span>
</label>
<label class="field" for="cvc">
<span class="field-label">CVC</span>
<input id="cvc" class="input" type="text" inputmode="numeric" placeholder="123" autocomplete="cc-csc" required />
<span class="err" data-err></span>
</label>
</div>
<div class="nav-row">
<button type="button" class="ghost" data-back>← Back</button>
<button type="button" class="cta" data-next>Review gift <span aria-hidden="true">→</span></button>
</div>
</fieldset>
<!-- STEP 4 -->
<fieldset class="panel is-hidden" data-step="4">
<h2 class="panel-h">Review your gift</h2>
<ul class="review" id="reviewList"></ul>
<p class="fineprint">By giving you agree to our donor terms. You can cancel a monthly gift anytime.</p>
<div class="nav-row">
<button type="button" class="ghost" data-back>← Back</button>
<button type="submit" class="cta cta-give" id="submitBtn">Give <span id="submitAmt">$51.85</span></button>
</div>
</fieldset>
<!-- SUCCESS -->
<fieldset class="panel is-hidden success" data-step="done">
<div class="success-burst" aria-hidden="true">🌾</div>
<h2 class="success-h">Thank you, <span id="thankName">friend</span>!</h2>
<p class="success-amt" id="successAmt">You gave $51.85</p>
<div class="success-impact" id="successImpact">That's 20 hot meals for neighbors in need.</div>
<p class="success-note">A receipt is on its way to your inbox. Your gift is doubled by our match. 💚</p>
<button type="button" class="ghost" id="restartBtn">Make another gift</button>
</fieldset>
</form>
</section>
<!-- RIGHT: trust / impact rail -->
<aside class="rail" aria-label="Campaign impact">
<div class="photo">
<div class="photo-cap">Volunteers packing boxes at the Eastside pantry</div>
</div>
<div class="thermo">
<div class="thermo-head">
<strong>Summer Match Campaign</strong>
<span id="goalPct">73%</span>
</div>
<div class="bar" role="progressbar" aria-valuenow="73" aria-valuemin="0" aria-valuemax="100" aria-label="Campaign progress">
<div class="bar-fill" id="barFill" style="width:73%"></div>
</div>
<div class="thermo-foot">
<span><strong>$146,200</strong> raised</span>
<span>of $200,000 goal</span>
</div>
</div>
<ul class="stats">
<li><strong>1.2M</strong><span>meals served in 2025</span></li>
<li><strong>38¢</strong><span>feeds one person</span></li>
<li><strong>96%</strong><span>goes to programs</span></li>
</ul>
<div class="donors">
<h3>Recent supporters</h3>
<ul id="donorList">
<li><span class="dot" aria-hidden="true"></span> Maya R. gave <b>$100</b> · monthly</li>
<li><span class="dot" aria-hidden="true"></span> The Okafor Family gave <b>$250</b></li>
<li><span class="dot" aria-hidden="true"></span> Anonymous gave <b>$50</b></li>
<li><span class="dot" aria-hidden="true"></span> Daniel T. gave <b>$25</b> · monthly</li>
</ul>
</div>
<p class="seal"><span aria-hidden="true">★</span> 4-star rated · Charity Transparency Council</p>
</aside>
</main>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Donation Flow
A complete giving experience for the fictional Open Harvest food charity, built as a multi-step flow with a sticky impact rail. The first step pairs amount presets and a custom field with a one-time / monthly toggle, a designation dropdown, and a cover-the-fees checkbox. As the donor changes the amount or frequency, a live impact banner re-renders to show the gift’s equivalence — hot meals, food boxes, or school lunches — with a small emoji pop micro-interaction.
Steps two through four collect donor details, payment, and a final review, each with inline validation: required names, a real email pattern, a card-number check, and an MM/YY expiry mask. The right rail reinforces transparency with a fundraising thermometer, headline impact numbers (meals served, cost per person, program ratio), a live donor wall, and registered-charity trust badges. Submitting simulates processing, then reveals an animated thank-you screen with the donor’s name and impact equivalence, bumps the thermometer, and adds the gift to the donor wall.
Everything is vanilla HTML, CSS, and JavaScript — no frameworks or build step — using a warm sand-and-teal palette, Fraunces headings, and a reusable toast() helper for confirmations.
Illustrative UI only — fictional organization, not a real charity or donation system.