Nonprofit — Fundraising Thermometer
A warm, self-contained fundraising progress UI for nonprofits and charity campaigns. Pairs a classic vertical thermometer with bulb and milestone markers alongside horizontal progress bars for multiple funds. Shows raised, goal, percent and donor count with transparent impact numbers, trust badges and donor recognition. Preset and custom donation buttons animate the fill, fire milestone toasts, and add recent givers in real time.
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 4px 12px rgba(42, 39, 34, 0.05);
--sh-md: 0 6px 22px rgba(42, 39, 34, 0.09), 0 2px 6px rgba(42, 39, 34, 0.05);
--sh-lg: 0 18px 48px rgba(21, 94, 84, 0.14);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1, h2, h3 {
font-family: "Fraunces", Georgia, serif;
line-height: 1.15;
margin: 0;
letter-spacing: -0.01em;
}
.wrap {
max-width: 1080px;
margin: 0 auto;
padding: 28px 22px 56px;
}
.visually-hidden {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
}
/* ---------- Masthead ---------- */
.masthead {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding-bottom: 20px;
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 12px; }
.brand-mark {
display: grid;
place-items: center;
width: 46px; height: 46px;
border-radius: 13px;
background: linear-gradient(140deg, var(--brand), var(--brand-d));
color: #fff;
font-family: "Fraunces", serif;
font-weight: 700;
font-size: 1.05rem;
box-shadow: var(--sh-sm);
}
.brand-name { margin: 0; font-weight: 700; font-size: 1.02rem; }
.brand-tag { margin: 0; color: var(--muted); font-size: 0.82rem; }
.trust { display: flex; gap: 8px; flex-wrap: wrap; }
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.74rem;
font-weight: 600;
color: var(--brand-d);
background: rgba(31, 122, 109, 0.09);
border: 1px solid rgba(31, 122, 109, 0.18);
padding: 5px 10px;
border-radius: 999px;
}
.badge .dot {
width: 7px; height: 7px;
border-radius: 50%;
background: var(--ok);
}
/* ---------- Hero ---------- */
.hero { padding: 30px 0 8px; }
.eyebrow {
margin: 0 0 10px;
text-transform: uppercase;
letter-spacing: 0.14em;
font-size: 0.72rem;
font-weight: 700;
color: var(--accent-d);
}
.hero h1 {
font-size: clamp(1.7rem, 4.6vw, 2.6rem);
font-weight: 600;
max-width: 18ch;
}
.hero h1 strong { color: var(--brand); font-weight: 700; }
.lede {
margin: 14px 0 0;
max-width: 56ch;
color: var(--ink-2);
font-size: 1.02rem;
}
.impact-row {
display: flex;
gap: 26px;
flex-wrap: wrap;
margin-top: 22px;
}
.impact { display: flex; flex-direction: column; }
.impact-num {
font-family: "Fraunces", serif;
font-weight: 700;
font-size: 1.5rem;
color: var(--brand-d);
line-height: 1.1;
}
.impact-lbl { font-size: 0.8rem; color: var(--muted); }
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: 320px 1fr;
gap: 22px;
margin-top: 30px;
align-items: start;
}
.col { display: flex; flex-direction: column; gap: 22px; }
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--sh-md);
padding: 22px;
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 16px;
}
.card-head h2, .card > h2 { font-size: 1.18rem; font-weight: 600; }
.chip {
font-size: 0.72rem;
font-weight: 600;
color: var(--muted);
background: var(--bg);
border: 1px solid var(--line);
padding: 4px 10px;
border-radius: 999px;
white-space: nowrap;
}
.chip-live { color: var(--brand-d); }
.chip-live .dot {
display: inline-block;
width: 7px; height: 7px;
border-radius: 50%;
background: var(--ok);
margin-right: 4px;
box-shadow: 0 0 0 0 rgba(47, 158, 111, 0.5);
animation: pulse 1.9s infinite;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(47, 158, 111, 0.45); }
70% { box-shadow: 0 0 0 7px rgba(47, 158, 111, 0); }
100% { box-shadow: 0 0 0 0 rgba(47, 158, 111, 0); }
}
/* ---------- Vertical thermometer ---------- */
.thermo-card { position: sticky; top: 16px; }
.thermo-stage {
display: flex;
justify-content: center;
gap: 14px;
padding: 14px 0 8px;
}
.milestones {
list-style: none;
margin: 0;
padding: 0;
position: relative;
width: 52px;
height: 300px;
align-self: flex-start;
margin-top: 6px;
}
.milestones li {
position: absolute;
bottom: var(--at);
right: 0;
transform: translateY(50%);
font-size: 0.72rem;
font-weight: 600;
color: var(--muted);
white-space: nowrap;
}
.milestones li::after {
content: "";
position: absolute;
right: -10px;
top: 50%;
width: 7px;
height: 1px;
background: var(--line-2);
}
.thermo {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.thermo-tube {
position: relative;
width: 46px;
height: 300px;
border-radius: 999px;
background: linear-gradient(180deg, #f0ebe2, #e7e0d4);
border: 1px solid var(--line-2);
box-shadow: inset 0 2px 6px rgba(42, 39, 34, 0.12);
overflow: hidden;
}
.thermo-fill {
position: absolute;
left: 0; right: 0; bottom: 0;
height: 0%;
background: linear-gradient(180deg, var(--accent), var(--accent-d));
border-radius: 999px 999px 0 0;
transition: height 0.95s cubic-bezier(0.22, 1, 0.36, 1);
box-shadow: 0 -2px 10px rgba(232, 116, 59, 0.4);
}
.thermo-shine {
position: absolute;
top: 0; left: 6px;
width: 8px;
height: 100%;
border-radius: 999px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0));
}
.thermo-marks { list-style: none; margin: 0; padding: 0; }
.thermo-marks .mark {
position: absolute;
left: 0; right: 0;
bottom: var(--at);
height: 1px;
background: rgba(42, 39, 34, 0.16);
}
.thermo-bulb {
position: relative;
width: 72px; height: 72px;
margin-top: -14px;
border-radius: 50%;
background: linear-gradient(140deg, var(--accent), var(--accent-d));
display: grid;
place-items: center;
color: #fff;
font-weight: 700;
font-size: 1.05rem;
font-family: "Fraunces", serif;
box-shadow: var(--sh-md), inset 0 2px 6px rgba(255, 255, 255, 0.3);
border: 4px solid var(--surface);
}
.thermo-stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-top: 18px;
padding-top: 16px;
border-top: 1px solid var(--line);
text-align: center;
}
.stat { display: flex; flex-direction: column; }
.stat-num {
font-family: "Fraunces", serif;
font-weight: 700;
font-size: 1.05rem;
color: var(--ink);
}
.stat-lbl { font-size: 0.7rem; color: var(--muted); }
/* ---------- Horizontal bars ---------- */
.bars { display: flex; flex-direction: column; gap: 18px; }
.bar-meta {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 8px;
margin-bottom: 7px;
}
.bar-name { font-weight: 600; font-size: 0.92rem; }
.bar-fig { font-size: 0.82rem; color: var(--muted); }
.bar-fig strong { color: var(--ink); }
.bar-track {
position: relative;
height: 22px;
border-radius: 999px;
background: #efe9df;
border: 1px solid var(--line);
overflow: hidden;
}
.bar-fill {
position: relative;
height: 100%;
width: 0%;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 9px;
transition: width 0.95s cubic-bezier(0.22, 1, 0.36, 1);
min-width: 30px;
}
.bar-fill.primary { background: linear-gradient(90deg, var(--brand), var(--brand-d)); }
.bar-fill.accent { background: linear-gradient(90deg, var(--accent), var(--accent-d)); }
.bar-fill.ok { background: linear-gradient(90deg, #46b487, var(--ok)); }
.bar-pct {
font-size: 0.7rem;
font-weight: 700;
color: #fff;
}
.bar-marks { list-style: none; margin: 0; padding: 0; }
.bar-marks li {
position: absolute;
top: 0; bottom: 0;
left: var(--at);
width: 1px;
background: rgba(255, 255, 255, 0.45);
z-index: 2;
}
/* ---------- Give card ---------- */
.give-sub { margin: 0 0 16px; color: var(--ink-2); font-size: 0.9rem; }
.give-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.give-btn {
font: inherit;
font-weight: 700;
cursor: pointer;
padding: 13px 8px;
border-radius: var(--r-md);
border: 1.5px solid var(--line-2);
background: var(--surface);
color: var(--brand-d);
transition: transform 0.12s ease, border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.give-btn:hover {
border-color: var(--brand);
background: rgba(31, 122, 109, 0.06);
box-shadow: var(--sh-sm);
}
.give-btn:active { transform: scale(0.96); }
.give-btn:focus-visible { outline: 3px solid rgba(31, 122, 109, 0.4); outline-offset: 2px; }
.custom {
display: flex;
gap: 10px;
margin-top: 14px;
}
.custom-input {
flex: 1;
display: flex;
align-items: center;
gap: 4px;
padding: 0 12px;
border-radius: var(--r-md);
border: 1.5px solid var(--line-2);
background: var(--surface);
color: var(--muted);
font-weight: 600;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.custom-input:focus-within {
border-color: var(--brand);
box-shadow: 0 0 0 3px rgba(31, 122, 109, 0.14);
}
.custom-input input {
flex: 1;
border: 0;
background: transparent;
font: inherit;
font-weight: 600;
color: var(--ink);
padding: 12px 0;
outline: none;
min-width: 0;
}
.donate-btn {
font: inherit;
font-weight: 700;
cursor: pointer;
padding: 12px 20px;
border-radius: var(--r-md);
border: 0;
color: #fff;
background: linear-gradient(135deg, var(--accent), var(--accent-d));
box-shadow: 0 6px 16px rgba(232, 116, 59, 0.32);
transition: transform 0.12s ease, box-shadow 0.15s ease, filter 0.15s ease;
white-space: nowrap;
}
.donate-btn:hover { filter: brightness(1.04); box-shadow: 0 8px 22px rgba(232, 116, 59, 0.4); }
.donate-btn:active { transform: scale(0.97); }
.donate-btn:focus-visible { outline: 3px solid rgba(232, 116, 59, 0.45); outline-offset: 2px; }
.reset-btn {
margin-top: 14px;
font: inherit;
font-size: 0.82rem;
font-weight: 600;
color: var(--muted);
background: none;
border: 0;
cursor: pointer;
padding: 4px 0;
text-decoration: underline;
text-underline-offset: 3px;
}
.reset-btn:hover { color: var(--ink); }
.reset-btn:focus-visible { outline: 2px solid var(--line-2); outline-offset: 3px; border-radius: 4px; }
/* ---------- Donors ---------- */
.donor-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.donor {
display: flex;
align-items: center;
gap: 11px;
padding: 9px 4px;
border-bottom: 1px solid var(--line);
}
.donor:last-child { border-bottom: 0; }
.donor.empty {
justify-content: center;
color: var(--muted);
font-size: 0.88rem;
font-style: italic;
border-bottom: 0;
padding: 18px 0;
}
.donor.is-new { animation: slideIn 0.5s ease; }
@keyframes slideIn {
from { opacity: 0; transform: translateY(-7px); background: rgba(232, 116, 59, 0.1); }
to { opacity: 1; transform: translateY(0); background: transparent; }
}
.donor-av {
width: 34px; height: 34px;
border-radius: 50%;
display: grid;
place-items: center;
color: #fff;
font-weight: 700;
font-size: 0.78rem;
flex: 0 0 auto;
}
.donor-info { flex: 1; min-width: 0; }
.donor-name { font-weight: 600; font-size: 0.9rem; }
.donor-when { font-size: 0.74rem; color: var(--muted); }
.donor-amt {
font-family: "Fraunces", serif;
font-weight: 700;
color: var(--brand-d);
font-size: 0.98rem;
}
/* ---------- Footer & toast ---------- */
.foot {
margin-top: 34px;
padding-top: 18px;
border-top: 1px solid var(--line);
text-align: center;
color: var(--muted);
font-size: 0.8rem;
}
.toast-host {
position: fixed;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 8px;
z-index: 60;
width: max-content;
max-width: 90vw;
}
.toast {
display: flex;
align-items: center;
gap: 9px;
background: var(--ink);
color: #fff;
padding: 11px 16px;
border-radius: 999px;
font-size: 0.86rem;
font-weight: 500;
box-shadow: var(--sh-lg);
animation: toastIn 0.3s ease;
}
.toast .toast-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--accent);
flex: 0 0 auto;
}
.toast.leaving { animation: toastOut 0.3s ease forwards; }
@keyframes toastIn {
from { opacity: 0; transform: translateY(12px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes toastOut {
to { opacity: 0; transform: translateY(12px) scale(0.96); }
}
.goal-hit .thermo-bulb {
animation: cheer 0.7s ease;
}
@keyframes cheer {
0%, 100% { transform: scale(1); }
35% { transform: scale(1.14) rotate(-4deg); }
65% { transform: scale(1.08) rotate(3deg); }
}
/* ---------- Responsive ---------- */
@media (max-width: 880px) {
.grid { grid-template-columns: 1fr; }
.thermo-card { position: static; }
}
@media (max-width: 520px) {
.wrap { padding: 20px 15px 44px; }
.hero { padding: 22px 0 4px; }
.impact-row { gap: 18px; }
.impact-num { font-size: 1.25rem; }
.give-grid { grid-template-columns: repeat(2, 1fr); }
.custom { flex-direction: column; }
.donate-btn { width: 100%; }
.card { padding: 18px; }
.thermo-tube, .milestones { height: 240px; }
.trust { width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.001ms !important; transition-duration: 0.15s !important; }
}(function () {
"use strict";
var GOAL = 60000;
var START_RAISED = 21450;
var START_DONORS = 312;
var state = {
raised: START_RAISED,
donors: START_DONORS,
};
// Donor name pools (fictional)
var FIRST = ["Maya", "Theo", "Priya", "Daniel", "Amara", "Noah", "Lena", "Ravi",
"Sofia", "Marcus", "Yuki", "Elena", "Omar", "Grace", "Jonas", "Aisha"];
var LAST = ["R.", "K.", "M.", "S.", "T.", "B.", "L.", "N.", "C.", "P."];
var AVATAR_COLORS = ["#1f7a6d", "#e8743b", "#2f9e6f", "#155e54", "#cc5d28", "#5a7d9a"];
var els = {
thermoFill: document.getElementById("thermoFill"),
thermoPct: document.getElementById("thermoPct"),
thermoCard: document.querySelector(".thermo-card"),
raisedVal: document.getElementById("raisedVal"),
goalVal: document.getElementById("goalVal"),
donorVal: document.getElementById("donorVal"),
barFill: document.getElementById("barFill"),
barPct: document.getElementById("barPct"),
barRaised: document.getElementById("barRaised"),
donorList: document.getElementById("donorList"),
donorCount: document.getElementById("donorCount"),
toastHost: document.getElementById("toastHost"),
};
var milestones = [
{ pct: 25, hit: false, label: "We're a quarter of the way — 3 villages funded!" },
{ pct: 50, hit: false, label: "Halfway there! Pumps ordered for 6 villages." },
{ pct: 75, hit: false, label: "75% reached — filter kits on the way!" },
{ pct: 100, hit: false, label: "Goal reached! Clean water for all 12 villages." },
];
function fmtMoney(n) {
return "$" + Math.round(n).toLocaleString("en-US");
}
function pct() {
return Math.min(100, (state.raised / GOAL) * 100);
}
function toast(msg, opts) {
opts = opts || {};
var el = document.createElement("div");
el.className = "toast";
var dot = document.createElement("span");
dot.className = "toast-dot";
if (opts.color) dot.style.background = opts.color;
var text = document.createElement("span");
text.textContent = msg;
el.appendChild(dot);
el.appendChild(text);
els.toastHost.appendChild(el);
var life = opts.duration || 3200;
setTimeout(function () {
el.classList.add("leaving");
setTimeout(function () {
if (el.parentNode) el.parentNode.removeChild(el);
}, 320);
}, life);
}
function render(animateMilestones) {
var p = pct();
var pRound = Math.round(p);
els.thermoFill.style.height = p + "%";
els.thermoPct.textContent = pRound + "%";
els.barFill.style.width = Math.max(p, 4) + "%";
els.barPct.textContent = pRound + "%";
els.raisedVal.textContent = fmtMoney(state.raised);
els.barRaised.textContent = fmtMoney(state.raised);
els.goalVal.textContent = fmtMoney(GOAL);
els.donorVal.textContent = state.donors.toLocaleString("en-US");
els.donorCount.textContent = state.donors.toLocaleString("en-US") + " gifts";
if (animateMilestones) {
milestones.forEach(function (m) {
if (!m.hit && p >= m.pct) {
m.hit = true;
var isGoal = m.pct === 100;
toast(m.label, {
color: isGoal ? "#2f9e6f" : "#e8743b",
duration: isGoal ? 5000 : 3600,
});
if (isGoal) {
els.thermoCard.classList.add("goal-hit");
setTimeout(function () {
els.thermoCard.classList.remove("goal-hit");
}, 800);
}
}
});
}
}
function addDonor(amount) {
var name = FIRST[Math.floor(Math.random() * FIRST.length)] + " " +
LAST[Math.floor(Math.random() * LAST.length)];
var color = AVATAR_COLORS[Math.floor(Math.random() * AVATAR_COLORS.length)];
var initials = name.charAt(0) + name.split(" ")[1].charAt(0);
var empty = els.donorList.querySelector(".empty");
if (empty) empty.remove();
var li = document.createElement("li");
li.className = "donor is-new";
var av = document.createElement("span");
av.className = "donor-av";
av.style.background = color;
av.textContent = initials;
av.setAttribute("aria-hidden", "true");
var info = document.createElement("div");
info.className = "donor-info";
var nm = document.createElement("div");
nm.className = "donor-name";
nm.textContent = name;
var when = document.createElement("div");
when.className = "donor-when";
when.textContent = "just now · Clean Water Fund";
info.appendChild(nm);
info.appendChild(when);
var amt = document.createElement("span");
amt.className = "donor-amt";
amt.textContent = fmtMoney(amount);
li.appendChild(av);
li.appendChild(info);
li.appendChild(amt);
els.donorList.insertBefore(li, els.donorList.firstChild);
// Cap visible list at 6
var items = els.donorList.querySelectorAll(".donor");
if (items.length > 6) {
els.donorList.removeChild(items[items.length - 1]);
}
}
function give(amount) {
if (!amount || amount < 1) return;
if (pct() >= 100) {
toast("The goal is already met — thank you! Reset the demo to give again.", {
color: "#2f9e6f",
});
return;
}
state.raised += amount;
state.donors += 1;
addDonor(amount);
render(true);
toast("Thanks for your " + fmtMoney(amount) + " gift!", { color: "#e8743b" });
}
// Donation preset buttons
var presetBtns = document.querySelectorAll(".give-btn");
presetBtns.forEach(function (btn) {
btn.addEventListener("click", function () {
give(parseInt(btn.getAttribute("data-amt"), 10));
});
});
// Custom amount form
var form = document.getElementById("customForm");
var input = document.getElementById("customAmt");
form.addEventListener("submit", function (e) {
e.preventDefault();
var val = parseInt(input.value, 10);
if (!val || val < 1) {
toast("Please enter an amount of $1 or more.", { color: "#d4503e" });
input.focus();
return;
}
give(val);
input.value = "";
});
// Reset demo
document.getElementById("resetBtn").addEventListener("click", function () {
state.raised = START_RAISED;
state.donors = START_DONORS;
milestones.forEach(function (m) { m.hit = false; });
els.donorList.innerHTML = '<li class="donor empty">Be the first to give today.</li>';
// Pre-mark milestones already passed at the starting point so they don't re-fire
var p = pct();
milestones.forEach(function (m) { if (p >= m.pct) m.hit = true; });
render(false);
toast("Demo reset to the campaign's current total.", { color: "#7a7368" });
});
// Initial paint — mark already-passed milestones, then animate from 0 to start.
els.goalVal.textContent = fmtMoney(GOAL);
var startPct = pct();
milestones.forEach(function (m) { if (startPct >= m.pct) m.hit = true; });
// Start visually at 0 so the fill animates up on load.
els.thermoFill.style.height = "0%";
els.barFill.style.width = "4%";
requestAnimationFrame(function () {
requestAnimationFrame(function () {
render(false);
});
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Riverbend Relief — Fundraising Thermometer</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="wrap">
<header class="masthead">
<div class="brand">
<span class="brand-mark" aria-hidden="true">RR</span>
<div>
<p class="brand-name">Riverbend Relief</p>
<p class="brand-tag">Clean water for every village</p>
</div>
</div>
<div class="trust">
<span class="badge"><span class="dot" aria-hidden="true"></span> Registered Charity #84-2291</span>
<span class="badge">Tax-deductible</span>
</div>
</header>
<section class="hero">
<div class="hero-copy">
<p class="eyebrow">Spring Water Drive</p>
<h1>Help us reach <strong>12 villages</strong> with safe drinking water.</h1>
<p class="lede">Every gift digs us closer to clean wells, working pumps, and healthier families across the Riverbend valley. Watch the thermometer climb as the community gives.</p>
<div class="impact-row" role="list">
<div class="impact" role="listitem">
<span class="impact-num" data-impact="meals">18,400</span>
<span class="impact-lbl">liters delivered daily</span>
</div>
<div class="impact" role="listitem">
<span class="impact-num" data-impact="villages">7</span>
<span class="impact-lbl">villages served</span>
</div>
<div class="impact" role="listitem">
<span class="impact-num" data-impact="vol">126</span>
<span class="impact-lbl">local volunteers</span>
</div>
</div>
</div>
</section>
<main class="grid">
<!-- Vertical thermometer card -->
<section class="card thermo-card" aria-labelledby="thermo-title">
<div class="card-head">
<h2 id="thermo-title">Clean Water Fund</h2>
<span class="chip chip-live"><span class="dot" aria-hidden="true"></span> Live</span>
</div>
<div class="thermo-stage">
<ul class="milestones" aria-hidden="true">
<li style="--at:100%"><span>$60k</span></li>
<li style="--at:75%"><span>$45k</span></li>
<li style="--at:50%"><span>$30k</span></li>
<li style="--at:25%"><span>$15k</span></li>
<li style="--at:0%"><span>$0</span></li>
</ul>
<div class="thermo" role="img" aria-label="Fundraising thermometer">
<div class="thermo-tube">
<div class="thermo-fill" id="thermoFill">
<span class="thermo-shine" aria-hidden="true"></span>
</div>
<ul class="thermo-marks" aria-hidden="true">
<li class="mark" style="--at:25%"></li>
<li class="mark" style="--at:50%"></li>
<li class="mark" style="--at:75%"></li>
</ul>
</div>
<div class="thermo-bulb">
<span id="thermoPct">0%</span>
</div>
</div>
</div>
<div class="thermo-stats">
<div class="stat">
<span class="stat-num" id="raisedVal">$0</span>
<span class="stat-lbl">raised so far</span>
</div>
<div class="stat">
<span class="stat-num" id="goalVal">$60,000</span>
<span class="stat-lbl">campaign goal</span>
</div>
<div class="stat">
<span class="stat-num" id="donorVal">0</span>
<span class="stat-lbl">donors</span>
</div>
</div>
</section>
<!-- Right column: bar variants + actions -->
<div class="col">
<section class="card bar-card" aria-labelledby="bars-title">
<div class="card-head">
<h2 id="bars-title">Campaign progress</h2>
<span class="chip">Updated just now</span>
</div>
<div class="bars">
<div class="bar-row">
<div class="bar-meta">
<span class="bar-name">Clean Water Fund</span>
<span class="bar-fig"><strong id="barRaised">$0</strong> / $60,000</span>
</div>
<div class="bar-track">
<div class="bar-fill primary" id="barFill">
<span class="bar-pct" id="barPct">0%</span>
</div>
<ul class="bar-marks" aria-hidden="true">
<li style="--at:25%"></li>
<li style="--at:50%"></li>
<li style="--at:75%"></li>
</ul>
</div>
</div>
<div class="bar-row">
<div class="bar-meta">
<span class="bar-name">School Pumps</span>
<span class="bar-fig"><strong>$8,250</strong> / $10,000</span>
</div>
<div class="bar-track">
<div class="bar-fill accent" style="width:82.5%"><span class="bar-pct">82%</span></div>
</div>
</div>
<div class="bar-row">
<div class="bar-meta">
<span class="bar-name">Filter Kits</span>
<span class="bar-fig"><strong>$3,900</strong> / $12,000</span>
</div>
<div class="bar-track">
<div class="bar-fill ok" style="width:32.5%"><span class="bar-pct">33%</span></div>
</div>
</div>
</div>
</section>
<section class="card give-card" aria-labelledby="give-title">
<h2 id="give-title">Move the thermometer</h2>
<p class="give-sub">Tap a gift to add it to the Clean Water Fund and watch the fill animate.</p>
<div class="give-grid" role="group" aria-label="Choose a donation amount">
<button class="give-btn" data-amt="25">$25</button>
<button class="give-btn" data-amt="50">$50</button>
<button class="give-btn" data-amt="100">$100</button>
<button class="give-btn" data-amt="250">$250</button>
<button class="give-btn" data-amt="500">$500</button>
<button class="give-btn" data-amt="1000">$1,000</button>
</div>
<form class="custom" id="customForm">
<label class="visually-hidden" for="customAmt">Custom amount</label>
<div class="custom-input">
<span aria-hidden="true">$</span>
<input id="customAmt" name="customAmt" type="number" min="1" step="1" inputmode="numeric" placeholder="Custom amount" />
</div>
<button type="submit" class="donate-btn">Donate</button>
</form>
<button type="button" class="reset-btn" id="resetBtn">Reset demo</button>
</section>
<section class="card donors-card" aria-labelledby="donors-title">
<div class="card-head">
<h2 id="donors-title">Recent givers</h2>
<span class="chip" id="donorCount">0 gifts</span>
</div>
<ul class="donor-list" id="donorList">
<li class="donor empty">Be the first to give today.</li>
</ul>
</section>
</div>
</main>
<footer class="foot">
<p>Riverbend Relief is a fictional organization. Gifts on this page are illustrative only.</p>
</footer>
</div>
<div class="toast-host" id="toastHost" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Fundraising Thermometer
A donation-progress pattern built for nonprofit and charity campaigns. A sticky vertical thermometer — tube, animated fill, glowing bulb and labelled milestone markers at $15k / $30k / $45k / $60k — sits beside a stack of horizontal progress bars that track several funds at once. Headline impact numbers, a registered-charity trust badge and a tax-deductible note reinforce transparency, while a donor-recognition list celebrates recent givers.
Interaction is driven entirely with vanilla JavaScript. Preset gift buttons ($25 through $1,000) and a custom-amount form each add to the Clean Water Fund, smoothly animate the thermometer and bar fills with a cubic-bezier ease, increment the raised total and donor count, and prepend a freshly generated giver to the recognition list. Crossing 25%, 50%, 75% and 100% fires celebratory milestone toasts, and reaching the goal triggers a brief cheer animation on the bulb.
Everything is themeable through CSS custom properties on :root, responsive down to ~360px, keyboard-usable and AA-contrast. A reset button restores the campaign to its current total so the demo can be replayed. No frameworks, no build step — three files you can drop into any landing page.
Illustrative UI only — fictional organization, not a real charity or donation system.