Nonprofit — Donation Widget
A warm, conversion-minded donation widget for charities and nonprofits. Preset amount chips with a custom-amount input, a one-time versus monthly frequency toggle, live impact-equivalence copy that changes per gift size, an optional cover-the-fee checkbox, a campaign progress thermometer, donor recognition, and trust badges. The donate button label updates to reflect the running total and frequency, and a toast confirms each gift, all in self-contained vanilla HTML, CSS, and JavaScript.
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 24px 60px -20px rgba(42, 39, 34, 0.28);
}
* { 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:
radial-gradient(1200px 600px at 12% -10%, rgba(31, 122, 109, 0.08), transparent 60%),
radial-gradient(1000px 500px at 110% 110%, rgba(232, 116, 59, 0.08), transparent 60%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
.wrap {
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(16px, 4vw, 48px);
}
.widget {
width: 100%;
max-width: 940px;
display: grid;
grid-template-columns: 1.05fr 0.95fr;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-lg);
overflow: hidden;
}
/* ---------- Mission panel ---------- */
.mission {
padding: clamp(22px, 3vw, 34px);
background: linear-gradient(180deg, #fcfaf6, #f7f1e9);
border-right: 1px solid var(--line);
}
.photo {
position: relative;
height: 150px;
border-radius: var(--r-md);
background:
linear-gradient(135deg, rgba(31, 122, 109, 0.85), rgba(21, 94, 84, 0.7)),
linear-gradient(50deg, #e8743b 0%, #f0a06b 40%, #1f7a6d 100%);
box-shadow: var(--sh-sm);
overflow: hidden;
margin-bottom: 18px;
}
.photo::after {
content: "";
position: absolute; inset: 0;
background:
radial-gradient(60px 60px at 30% 60%, rgba(255, 255, 255, 0.22), transparent 70%),
radial-gradient(50px 50px at 62% 45%, rgba(255, 255, 255, 0.18), transparent 70%),
radial-gradient(70px 70px at 80% 70%, rgba(0, 0, 0, 0.12), transparent 70%);
}
.photo-cap {
position: absolute;
left: 12px; bottom: 10px;
z-index: 1;
font-size: 11.5px;
color: #fff;
background: rgba(21, 94, 84, 0.55);
backdrop-filter: blur(4px);
padding: 4px 9px;
border-radius: 999px;
font-weight: 500;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11.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;
margin-bottom: 12px;
}
.mission h1 {
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
font-size: clamp(24px, 3.4vw, 30px);
line-height: 1.15;
margin: 4px 0 8px;
letter-spacing: -0.01em;
}
.lede {
margin: 0 0 18px;
color: var(--ink-2);
font-size: 14.5px;
}
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 20px;
}
.stat {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-sm);
padding: 10px 6px;
text-align: center;
}
.stat strong {
display: block;
font-family: "Fraunces", serif;
font-size: 19px;
color: var(--brand-d);
line-height: 1.1;
}
.stat span {
font-size: 11px;
color: var(--muted);
}
.thermo-head, .thermo-foot {
display: flex;
justify-content: space-between;
align-items: baseline;
font-size: 12.5px;
color: var(--ink-2);
}
.thermo-head strong { color: var(--brand-d); font-size: 16px; }
.thermo-head .goal { color: var(--muted); }
.thermo-track {
height: 12px;
background: rgba(42, 39, 34, 0.08);
border-radius: 999px;
overflow: hidden;
margin: 6px 0;
box-shadow: inset 0 1px 2px rgba(42, 39, 34, 0.08);
}
.thermo-fill {
height: 100%;
width: var(--pct);
border-radius: 999px;
background: linear-gradient(90deg, var(--brand), #34a08f 70%, var(--accent));
transition: width 0.9s cubic-bezier(0.22, 1, 0.36, 1);
}
.thermo-foot { color: var(--muted); margin-top: 2px; }
.thermo-foot span:first-child { color: var(--brand-d); font-weight: 600; }
/* ---------- Give form ---------- */
.give {
padding: clamp(22px, 3vw, 34px);
display: flex;
flex-direction: column;
}
.give h2 {
font-family: "Fraunces", serif;
font-weight: 600;
font-size: 21px;
margin: 0 0 16px;
}
.freq {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
padding: 4px;
background: rgba(42, 39, 34, 0.05);
border-radius: 999px;
margin-bottom: 18px;
}
.freq-btn {
border: 0;
background: transparent;
font: inherit;
font-weight: 600;
font-size: 14px;
color: var(--ink-2);
padding: 9px 8px;
border-radius: 999px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: background 0.18s, color 0.18s, box-shadow 0.18s;
}
.freq-btn:hover { color: var(--ink); }
.freq-btn.is-active {
background: var(--surface);
color: var(--brand-d);
box-shadow: var(--sh-sm);
}
.freq-tag {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.02em;
color: var(--accent-d);
background: rgba(232, 116, 59, 0.14);
padding: 2px 6px;
border-radius: 999px;
}
.amounts {
border: 0;
padding: 0;
margin: 0 0 16px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 9px;
}
.chip {
appearance: none;
border: 1.5px solid var(--line-2);
background: var(--surface);
font: inherit;
font-weight: 700;
font-size: 15px;
color: var(--ink);
padding: 12px 6px;
border-radius: var(--r-md);
cursor: pointer;
text-align: center;
transition: border-color 0.15s, background 0.15s, transform 0.08s, box-shadow 0.15s;
}
.chip:hover {
border-color: var(--brand);
background: rgba(31, 122, 109, 0.05);
}
.chip:active { transform: translateY(1px) scale(0.99); }
.chip.is-active {
border-color: var(--brand);
background: rgba(31, 122, 109, 0.1);
color: var(--brand-d);
box-shadow: 0 0 0 3px rgba(31, 122, 109, 0.13);
}
.chip-custom {
display: flex;
align-items: center;
gap: 2px;
padding: 0 12px;
grid-column: span 3;
}
.chip-custom .cur {
color: var(--muted);
font-weight: 700;
}
.chip-custom input {
flex: 1;
border: 0;
background: transparent;
font: inherit;
font-weight: 700;
font-size: 15px;
color: var(--ink);
padding: 12px 0;
outline: none;
min-width: 0;
}
.chip-custom input::placeholder { color: var(--muted); font-weight: 600; }
.impact {
display: flex;
gap: 11px;
align-items: flex-start;
background: rgba(31, 122, 109, 0.07);
border: 1px solid rgba(31, 122, 109, 0.16);
border-radius: var(--r-md);
padding: 12px 14px;
margin-bottom: 14px;
}
.impact-icon { color: var(--brand); flex-shrink: 0; margin-top: 1px; }
.impact p { margin: 0; font-size: 13.5px; color: var(--ink-2); }
.impact strong { color: var(--brand-d); }
.cover {
display: flex;
gap: 9px;
align-items: flex-start;
font-size: 13px;
color: var(--ink-2);
margin-bottom: 16px;
cursor: pointer;
}
.cover input { margin-top: 3px; accent-color: var(--brand); width: 16px; height: 16px; }
.cover em { color: var(--muted); font-style: normal; }
.donate {
border: 0;
font: inherit;
font-weight: 700;
font-size: 16.5px;
color: #fff;
background: linear-gradient(180deg, var(--accent), var(--accent-d));
padding: 15px;
border-radius: var(--r-md);
cursor: pointer;
box-shadow: 0 8px 20px -6px rgba(232, 116, 59, 0.6);
transition: transform 0.1s, box-shadow 0.18s, filter 0.18s;
}
.donate:hover { filter: brightness(1.04); box-shadow: 0 12px 26px -6px rgba(232, 116, 59, 0.65); }
.donate:active { transform: translateY(1px); }
.donate:focus-visible { outline: 3px solid rgba(31, 122, 109, 0.5); outline-offset: 2px; }
.assure {
list-style: none;
margin: 14px 0 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 6px 16px;
font-size: 11.5px;
color: var(--muted);
}
.assure li { display: inline-flex; align-items: center; gap: 5px; }
.assure svg { color: var(--ok); }
.donors {
margin-top: 18px;
padding-top: 16px;
border-top: 1px solid var(--line);
}
.donors-label {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--muted);
}
.donor-list {
list-style: none;
margin: 8px 0 0;
padding: 0;
display: grid;
gap: 7px;
}
.donor-list li {
display: flex;
align-items: center;
gap: 9px;
font-size: 13px;
color: var(--ink-2);
}
.donor-list b { color: var(--ink); }
.av {
width: 26px; height: 26px;
flex-shrink: 0;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 10px;
font-weight: 700;
color: #fff;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
}
.donor-list li:nth-child(2) .av { background: linear-gradient(135deg, var(--accent), var(--accent-d)); }
.donor-list li:nth-child(3) .av { background: linear-gradient(135deg, #5a8fb8, #3f6f95); }
@keyframes flashIn {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
.donor-list li.is-new { animation: flashIn 0.4s ease; }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 20px);
background: var(--ink);
color: #fff;
font-size: 13.5px;
font-weight: 500;
padding: 12px 18px;
border-radius: 999px;
box-shadow: var(--sh-md);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s, transform 0.25s;
z-index: 50;
max-width: 90vw;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 760px) {
.widget { grid-template-columns: 1fr; }
.mission { border-right: 0; border-bottom: 1px solid var(--line); }
}
@media (max-width: 520px) {
.stats { gap: 7px; }
.stat strong { font-size: 17px; }
.amounts { grid-template-columns: repeat(3, 1fr); }
.mission h1 { font-size: 23px; }
.assure { font-size: 11px; }
}
@media (max-width: 360px) {
.amounts { grid-template-columns: repeat(2, 1fr); }
.chip-custom { grid-column: span 2; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition: none !important; animation: none !important; }
}(function () {
"use strict";
var form = document.getElementById("giveForm");
var chips = Array.prototype.slice.call(document.querySelectorAll(".chip[data-amount]"));
var customInput = document.getElementById("customAmt");
var customChip = customInput.closest(".chip-custom");
var freqBtns = Array.prototype.slice.call(document.querySelectorAll(".freq-btn"));
var donateBtn = document.getElementById("donateBtn");
var impactText = document.getElementById("impactText");
var coverFee = document.getElementById("coverFee");
var coverAmt = document.getElementById("coverAmt");
var donorList = document.getElementById("donorList");
var toastEl = document.getElementById("toast");
var state = { amount: 50, frequency: "once" };
// Impact equivalence tiers — chosen by gift size & frequency.
function impactFor(amount, freq) {
if (!amount || amount < 1) return "Every dollar funds the next pump part, pipe, and filter.";
var monthly = freq === "monthly";
if (amount < 25) return "Buys <strong>water-purification tablets</strong> for a household for a month.";
if (amount < 50) return monthly
? "Keeps a <strong>handpump maintained</strong> all year long."
: "Provides <strong>one month of clean water</strong> for a family of five.";
if (amount < 100) return monthly
? "Sponsors <strong>safe water for a family</strong>, refilled every month."
: "Delivers a <strong>household water filter</strong> that lasts two years.";
if (amount < 250) return monthly
? "Funds <strong>a village caretaker's stipend</strong> to keep wells flowing."
: "Trains <strong>two local technicians</strong> to repair community wells.";
if (amount < 500) return monthly
? "Builds <strong>a new well every year</strong> through your sustained giving."
: "Lays the <strong>pipework for a school tap stand</strong> serving 300 children.";
return monthly
? "Sponsors a <strong>full village water system</strong>, sustained month after month."
: "Funds <strong>an entire borehole well</strong> for a community of 250 people.";
}
function money(n) {
return "$" + Number(n).toLocaleString("en-US", { maximumFractionDigits: 2 });
}
function feeFor(amount) {
return Math.round(amount * 0.03 * 100) / 100;
}
function total() {
var amt = state.amount || 0;
return coverFee.checked ? amt + feeFor(amt) : amt;
}
function render() {
impactText.innerHTML = impactFor(state.amount, state.frequency);
var fee = feeFor(state.amount || 0);
coverAmt.textContent = "(+" + money(fee) + ")";
var label;
if (!state.amount || state.amount < 1) {
label = "Enter an amount";
donateBtn.disabled = true;
donateBtn.style.opacity = "0.6";
donateBtn.style.cursor = "not-allowed";
} else {
donateBtn.disabled = false;
donateBtn.style.opacity = "";
donateBtn.style.cursor = "";
label = "Donate " + money(total()) + (state.frequency === "monthly" ? " / month" : "");
}
donateBtn.textContent = label;
}
function clearChips() {
chips.forEach(function (c) { c.classList.remove("is-active"); });
customChip.classList.remove("is-active");
}
chips.forEach(function (chip) {
chip.addEventListener("click", function () {
clearChips();
chip.classList.add("is-active");
customInput.value = "";
state.amount = Number(chip.dataset.amount);
render();
});
});
customInput.addEventListener("input", function () {
var val = parseFloat(customInput.value);
clearChips();
customChip.classList.add("is-active");
state.amount = isNaN(val) ? 0 : Math.max(0, val);
render();
});
customInput.addEventListener("focus", function () {
clearChips();
customChip.classList.add("is-active");
var val = parseFloat(customInput.value);
state.amount = isNaN(val) ? 0 : val;
render();
});
freqBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
freqBtns.forEach(function (b) {
b.classList.remove("is-active");
b.setAttribute("aria-selected", "false");
});
btn.classList.add("is-active");
btn.setAttribute("aria-selected", "true");
state.frequency = btn.dataset.freq;
render();
});
});
coverFee.addEventListener("change", render);
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
}, 3200);
}
function initials(name) {
return name.split(" ").map(function (p) { return p[0]; }).join("").slice(0, 2).toUpperCase();
}
var givers = ["You", "Maya R.", "Tomás L.", "Aisha B.", "Noah W.", "Lena F."];
form.addEventListener("submit", function (e) {
e.preventDefault();
if (!state.amount || state.amount < 1) {
customInput.focus();
toast("Please choose or enter a donation amount.");
return;
}
var name = givers[Math.floor(Math.random() * givers.length)];
var li = document.createElement("li");
li.className = "is-new";
var freqTag = state.frequency === "monthly" ? " · monthly" : "";
li.innerHTML = '<span class="av" aria-hidden="true">' + initials(name) +
"</span> " + name + " gave <b>" + money(total()) + "</b>" + freqTag;
donorList.insertBefore(li, donorList.firstChild);
while (donorList.children.length > 4) {
donorList.removeChild(donorList.lastChild);
}
toast("Thank you! " + money(total()) +
(state.frequency === "monthly" ? " / month" : "") +
" brings clean water closer. 💧");
});
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bright Wells Foundation — Give Clean Water</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>
<main class="wrap">
<section class="widget" aria-labelledby="give-title">
<!-- Left: mission / impact -->
<aside class="mission">
<div class="photo" role="img" aria-label="Community members gathering at a newly built village well">
<span class="photo-cap">Amara & her neighbors, Kano Province — well #214</span>
</div>
<span class="badge">
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 2 4 5v6c0 5 3.4 8.7 8 11 4.6-2.3 8-6 8-11V5l-8-3Zm-1 14-4-4 1.4-1.4L11 13.2l4.6-4.6L17 10l-6 6Z"/></svg>
Registered charity · Reg. #84-2210193 · Tax-deductible
</span>
<h1 id="give-title">Bring clean water<br />to a village this year</h1>
<p class="lede">87% of every gift goes straight to the field. Help us reach the 11 communities still hauling water from the river each morning.</p>
<div class="stats" role="list">
<div class="stat" role="listitem"><strong>412,900</strong><span>people reached</span></div>
<div class="stat" role="listitem"><strong>1,083</strong><span>wells built</span></div>
<div class="stat" role="listitem"><strong>23</strong><span>countries</span></div>
</div>
<div class="thermo" aria-label="Campaign progress">
<div class="thermo-head">
<span><strong id="raised">$68,420</strong> raised</span>
<span class="goal">Goal $90,000</span>
</div>
<div class="thermo-track"><div class="thermo-fill" id="thermoFill" style="--pct:76%"></div></div>
<div class="thermo-foot"><span id="pctLabel">76% funded</span><span>14 days left</span></div>
</div>
</aside>
<!-- Right: donation form -->
<form class="give" id="giveForm" novalidate>
<h2>Make a gift</h2>
<div class="freq" role="tablist" aria-label="Donation frequency">
<button type="button" class="freq-btn is-active" role="tab" aria-selected="true" data-freq="once">One-time</button>
<button type="button" class="freq-btn" role="tab" aria-selected="false" data-freq="monthly">
Monthly <span class="freq-tag">2× impact</span>
</button>
</div>
<fieldset class="amounts">
<legend class="sr-only">Choose an amount</legend>
<button type="button" class="chip" data-amount="25">$25</button>
<button type="button" class="chip is-active" data-amount="50">$50</button>
<button type="button" class="chip" data-amount="100">$100</button>
<button type="button" class="chip" data-amount="250">$250</button>
<button type="button" class="chip" data-amount="500">$500</button>
<label class="chip chip-custom" for="customAmt">
<span class="cur">$</span>
<input id="customAmt" type="number" min="1" step="1" inputmode="numeric" placeholder="Other" aria-label="Custom amount in dollars" />
</label>
</fieldset>
<div class="impact" id="impact" aria-live="polite">
<span class="impact-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20"><path fill="currentColor" d="M12 2s6 6.5 6 11a6 6 0 0 1-12 0c0-4.5 6-11 6-11Z"/></svg>
</span>
<p id="impactText">Provides <strong>one month of clean water</strong> for a family of five.</p>
</div>
<label class="cover">
<input type="checkbox" id="coverFee" />
<span>Add 3% so 100% of my gift reaches the field <em id="coverAmt">(+$1.50)</em></span>
</label>
<button type="submit" class="donate" id="donateBtn">Donate $50</button>
<ul class="assure">
<li><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 1 3 5v6c0 5.2 3.4 9.4 9 11 5.6-1.6 9-5.8 9-11V5l-9-4Zm-1.2 14.2L7 11.4l1.4-1.4 2.4 2.4 5-5L17.2 9l-6.4 6.2Z"/></svg> Secure 256-bit encryption</li>
<li><svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><path fill="currentColor" d="M12 2 4 5v6c0 5 3.4 8.7 8 11 4.6-2.3 8-6 8-11V5l-8-3Z"/></svg> Tax-deductible receipt by email</li>
</ul>
<div class="donors">
<span class="donors-label">Recent givers</span>
<ul class="donor-list" id="donorList">
<li><span class="av" aria-hidden="true">DM</span> Diego M. gave <b>$100</b> · monthly</li>
<li><span class="av" aria-hidden="true">SK</span> Sarah K. gave <b>$50</b></li>
<li><span class="av" aria-hidden="true">PT</span> Priya T. gave <b>$250</b></li>
</ul>
</div>
</form>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Donation Widget
A reusable, drop-in donation panel built for the warm, hopeful tone of a clean-water charity. A mission column carries an impact photo, a registered-charity trust badge, headline impact numbers, and a fundraising thermometer; the giving column holds the actual ask. Five preset amount chips ($25–$500) sit alongside a custom-amount field, and a pill toggle switches between one-time and monthly gifts.
The widget reacts in real time. Selecting a chip or typing a custom amount rewrites the impact-equivalence line — “one month of clean water for a family of five,” “an entire borehole well for a community of 250” — and that copy adapts to whether the donor chose one-time or monthly. The donate button always shows the current total and frequency, an optional checkbox adds 3% to cover processing fees, and submitting fires a thank-you toast while prepending the gift to a live donor-recognition list.
Everything is vanilla JS with no dependencies: keyboard-usable buttons and inputs, aria-live impact updates, AA-contrast colors, and a layout that collapses cleanly down to 360px. Swap the org name, amounts, and impact tiers to fit any campaign.
Illustrative UI only — fictional organization, not a real charity or donation system.