Nonprofit — Impact Stat Counter
A warm, mission-driven impact stat counter for nonprofits and charities. A responsive grid of bold count-up figures pairs each number with an icon, label, and human context line such as meals served, wells repaired, and students kept in school. Numbers animate from zero with eased easing the moment the grid scrolls into view, and a replay button lets visitors watch the impact build again. Includes trust badges, a prominent donate call to action, and a toast helper.
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;
--shadow-sm: 0 1px 2px rgba(42, 39, 34, 0.06), 0 2px 8px rgba(42, 39, 34, 0.05);
--shadow-md: 0 6px 22px rgba(42, 39, 34, 0.1), 0 2px 6px rgba(42, 39, 34, 0.06);
--shadow-lg: 0 18px 48px rgba(42, 39, 34, 0.14);
}
*,
*::before,
*::after {
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(1100px 520px at 110% -10%, rgba(31, 122, 109, 0.08), transparent 60%),
radial-gradient(900px 480px at -10% 10%, rgba(232, 116, 59, 0.08), transparent 55%),
var(--bg);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
.page {
max-width: 1060px;
margin: 0 auto;
padding: clamp(28px, 6vw, 72px) clamp(16px, 4vw, 32px) 64px;
}
/* ---------- header ---------- */
.impact__head {
max-width: 640px;
margin-bottom: clamp(28px, 5vw, 48px);
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--brand-d);
background: rgba(31, 122, 109, 0.1);
border: 1px solid rgba(31, 122, 109, 0.18);
padding: 6px 12px;
border-radius: 999px;
}
.eyebrow svg { color: var(--accent); }
h1 {
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
font-size: clamp(1.9rem, 5.2vw, 3rem);
line-height: 1.08;
letter-spacing: -0.015em;
margin: 16px 0 12px;
color: var(--ink);
}
.impact__lede {
color: var(--ink-2);
font-size: clamp(1rem, 2.3vw, 1.12rem);
margin: 0 0 22px;
}
.impact__controls {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
.impact__hint {
font-size: 0.85rem;
color: var(--muted);
}
/* ---------- buttons ---------- */
.btn {
display: inline-flex;
align-items: center;
gap: 9px;
font: inherit;
font-weight: 600;
border: none;
cursor: pointer;
border-radius: 999px;
padding: 11px 18px;
text-decoration: none;
transition: transform 0.14s ease, box-shadow 0.18s ease, background 0.18s ease;
-webkit-tap-highlight-color: transparent;
}
.btn:active { transform: translateY(1px) scale(0.99); }
.btn:focus-visible {
outline: 3px solid rgba(31, 122, 109, 0.4);
outline-offset: 2px;
}
.btn--ghost {
background: var(--surface);
color: var(--brand-d);
border: 1px solid var(--line-2);
box-shadow: var(--shadow-sm);
}
.btn--ghost:hover {
border-color: var(--brand);
color: var(--brand);
box-shadow: var(--shadow-md);
}
.btn--ghost.is-spinning svg { animation: spin 0.7s ease; }
@keyframes spin {
from { transform: rotate(0); }
to { transform: rotate(-360deg); }
}
.btn--donate {
background: linear-gradient(180deg, var(--accent), var(--accent-d));
color: #fff;
box-shadow: 0 8px 22px rgba(232, 116, 59, 0.36);
font-size: 1rem;
padding: 13px 22px;
}
.btn--donate:hover {
box-shadow: 0 12px 30px rgba(232, 116, 59, 0.46);
transform: translateY(-1px);
}
/* ---------- stat grid ---------- */
.stats {
list-style: none;
margin: 0;
padding: 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: clamp(14px, 2vw, 20px);
}
.stat {
position: relative;
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 26px 22px 24px;
box-shadow: var(--shadow-sm);
overflow: hidden;
transform: translateY(14px);
opacity: 0;
transition: transform 0.55s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.55s ease, box-shadow 0.2s ease, border-color 0.2s ease;
}
.stat::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 4px;
background: var(--c, var(--brand));
opacity: 0.85;
}
.stat.is-in {
transform: translateY(0);
opacity: 1;
}
.stat:hover {
border-color: color-mix(in srgb, var(--c, var(--brand)) 45%, var(--line-2));
box-shadow: var(--shadow-md);
transform: translateY(-3px);
}
.stat__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 50px;
height: 50px;
border-radius: var(--r-md);
color: var(--c, var(--brand));
background: color-mix(in srgb, var(--c, var(--brand)) 14%, var(--surface));
margin-bottom: 16px;
}
.stat__num {
display: block;
font-family: "Fraunces", Georgia, serif;
font-weight: 600;
font-size: clamp(2.1rem, 5vw, 2.7rem);
line-height: 1;
letter-spacing: -0.02em;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.stat__num.is-counting { color: var(--c, var(--brand)); }
.stat__label {
display: block;
font-weight: 700;
font-size: 1.02rem;
margin-top: 8px;
color: var(--ink);
}
.stat__sub {
display: block;
font-size: 0.88rem;
color: var(--muted);
margin-top: 3px;
}
/* ---------- footer ---------- */
.impact__foot {
margin-top: clamp(26px, 4vw, 38px);
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
padding-top: 24px;
border-top: 1px solid var(--line);
}
.badges {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.82rem;
font-weight: 600;
color: var(--ink-2);
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 6px 12px;
}
.badge svg { color: var(--ok); }
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
background: var(--ink);
color: #fff;
font-size: 0.9rem;
font-weight: 500;
padding: 11px 18px;
border-radius: 999px;
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 50;
max-width: 90vw;
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* ---------- responsive ---------- */
@media (max-width: 820px) {
.stats { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 520px) {
.stats { grid-template-columns: 1fr; }
.impact__foot { flex-direction: column; align-items: flex-start; }
.btn--donate { width: 100%; justify-content: center; }
}
@media (prefers-reduced-motion: reduce) {
.stat { transition: opacity 0.3s ease; transform: none; }
.btn--ghost.is-spinning svg { animation: none; }
}(function () {
"use strict";
var grid = document.getElementById("statGrid");
var nums = Array.prototype.slice.call(document.querySelectorAll(".stat__num"));
var replayBtn = document.getElementById("replayBtn");
var donateBtn = document.getElementById("donateBtn");
var toastEl = document.getElementById("toast");
var reduceMotion =
window.matchMedia &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches;
var toastTimer;
function toast(msg) {
if (!toastEl) return;
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 2600);
}
// Group digits with thousands separators while preserving decimals.
function group(numStr) {
var parts = numStr.split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
}
function format(value, el) {
var decimals = parseInt(el.dataset.decimals || "0", 10);
var prefix = el.dataset.prefix || "";
var suffix = el.dataset.suffix || "";
return prefix + group(value.toFixed(decimals)) + suffix;
}
// easeOutCubic
function ease(t) {
return 1 - Math.pow(1 - t, 3);
}
function countUp(el, duration) {
var target = parseFloat(el.dataset.target);
if (isNaN(target)) return;
if (el._raf) cancelAnimationFrame(el._raf);
if (reduceMotion) {
el.textContent = format(target, el);
return;
}
el.classList.add("is-counting");
var start = null;
var dur = duration || 1500;
function step(ts) {
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var current = target * ease(p);
el.textContent = format(current, el);
if (p < 1) {
el._raf = requestAnimationFrame(step);
} else {
el.textContent = format(target, el);
el.classList.remove("is-counting");
el._raf = null;
}
}
el._raf = requestAnimationFrame(step);
}
var hasRun = false;
function runAll(stagger) {
nums.forEach(function (el, i) {
var delay = stagger ? i * 130 : 0;
setTimeout(function () {
countUp(el, 1500);
}, delay);
});
hasRun = true;
}
// Reveal cards + trigger counters when the grid scrolls into view.
var cards = Array.prototype.slice.call(grid.querySelectorAll(".stat"));
function revealCards() {
cards.forEach(function (card, i) {
setTimeout(function () {
card.classList.add("is-in");
}, reduceMotion ? 0 : i * 90);
});
}
if ("IntersectionObserver" in window) {
var io = new IntersectionObserver(
function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting && !hasRun) {
revealCards();
runAll(true);
io.disconnect();
}
});
},
{ threshold: 0.35 }
);
io.observe(grid);
} else {
revealCards();
runAll(true);
}
// Replay button — restart everything.
replayBtn.addEventListener("click", function () {
replayBtn.classList.remove("is-spinning");
// force reflow so the animation can re-trigger
void replayBtn.offsetWidth;
replayBtn.classList.add("is-spinning");
nums.forEach(function (el) {
el.textContent = "0";
});
runAll(true);
toast("Replaying impact for 2024 — thank you for caring");
});
donateBtn.addEventListener("click", function (e) {
e.preventDefault();
toast("Demo only — in production this opens the donation form ❤");
});
// Make stat cards keyboard-focusable for screen-reader/tab users.
cards.forEach(function (card) {
var label = card.querySelector(".stat__label");
if (label) {
card.setAttribute("tabindex", "0");
card.setAttribute("role", "group");
card.setAttribute("aria-label", label.textContent);
}
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Bright Harvest Alliance — Our Impact</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,wght@9..144,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="page">
<section class="impact" aria-labelledby="impact-heading">
<header class="impact__head">
<span class="eyebrow">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M12 21s-7.5-4.6-10-9.3C.2 8 2.2 4.5 5.7 4.5c2 0 3.4 1.1 4.3 2.5.9-1.4 2.3-2.5 4.3-2.5 3.5 0 5.5 3.5 3.7 7.2C19.5 16.4 12 21 12 21z"/></svg>
2024 Annual Impact
</span>
<h1 id="impact-heading">Every gift, counted in lives changed</h1>
<p class="impact__lede">
Bright Harvest Alliance turns generosity into meals, clean water, and second chances.
Here is what our community of donors made possible last year — transparent, audited, real.
</p>
<div class="impact__controls">
<button id="replayBtn" class="btn btn--ghost" type="button" aria-label="Replay the impact counters">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M12 5V2L7 6l5 4V7a5 5 0 1 1-5 5H5a7 7 0 1 0 7-7z"/></svg>
Replay counters
</button>
<span class="impact__hint" id="scrollHint">Counters animate as they scroll into view</span>
</div>
</header>
<ul class="stats" id="statGrid" role="list">
<li class="stat" style="--c:var(--accent)">
<span class="stat__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="26" height="26"><path fill="currentColor" d="M11 2v9H9V8H7v3a5 5 0 0 0 4 4.9V22h2v-6.1A5 5 0 0 0 17 11V8h-2v3h-2V2h-2zm6 0v9h2V2h-2z"/></svg>
</span>
<span class="stat__num" data-target="248930" data-suffix="">0</span>
<span class="stat__label">Hot meals served</span>
<span class="stat__sub">across 38 community kitchens</span>
</li>
<li class="stat" style="--c:var(--brand)">
<span class="stat__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="26" height="26"><path fill="currentColor" d="M12 2s6 7 6 11a6 6 0 1 1-12 0c0-4 6-11 6-11zm0 16a4 4 0 0 0 4-4h-2a2 2 0 0 1-2 2v2z"/></svg>
</span>
<span class="stat__num" data-target="1740" data-suffix="">0</span>
<span class="stat__label">Wells & taps repaired</span>
<span class="stat__sub">clean water for 21 villages</span>
</li>
<li class="stat" style="--c:#8a5cd0">
<span class="stat__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="26" height="26"><path fill="currentColor" d="M12 3 1 9l4 2.2V17c0 1.7 3.1 3 7 3s7-1.3 7-3v-5.8l2-1.1V17h2V9L12 3zm0 9.5L5 8.7 12 5l7 3.7-7 3.8z"/></svg>
</span>
<span class="stat__num" data-target="6125" data-suffix="">0</span>
<span class="stat__label">Students kept in school</span>
<span class="stat__sub">with fees, books & a daily lunch</span>
</li>
<li class="stat" style="--c:var(--ok)">
<span class="stat__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="26" height="26"><path fill="currentColor" d="M16 11a4 4 0 1 0-4-4 4 4 0 0 0 4 4zm-8 0a4 4 0 1 0-4-4 4 4 0 0 0 4 4zm0 2c-2.7 0-8 1.3-8 4v3h9v-2.5c0-1.2.5-2.4 1.4-3.4A13 13 0 0 0 8 13zm8 0c-.5 0-1 0-1.6.1 1.6 1.1 2.6 2.5 2.6 3.9V20h7v-3c0-2.7-5.3-4-8-4z"/></svg>
</span>
<span class="stat__num" data-target="3.1" data-decimals="1" data-suffix="M">0</span>
<span class="stat__label">Volunteer hours given</span>
<span class="stat__sub">by 14,200 neighbours like you</span>
</li>
<li class="stat" style="--c:var(--accent-d)">
<span class="stat__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="26" height="26"><path fill="currentColor" d="M3 13h2v7H3v-7zm4-3h2v10H7V10zm4-4h2v14h-2V6zm4 6h2v8h-2v-8zm4-9h2v17h-2V3z"/></svg>
</span>
<span class="stat__num" data-prefix="$" data-target="92" data-suffix="¢">0</span>
<span class="stat__label">Of every dollar to programs</span>
<span class="stat__sub">8¢ runs the whole operation</span>
</li>
<li class="stat" style="--c:#3b7fd0">
<span class="stat__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="26" height="26"><path fill="currentColor" d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8zm1-13h-2v6l5 3 1-1.7-4-2.3V7z"/></svg>
</span>
<span class="stat__num" data-target="14" data-suffix="" data-prefix="">0</span>
<span class="stat__label">Countries reached</span>
<span class="stat__sub">on four continents in 2024</span>
</li>
</ul>
<footer class="impact__foot">
<div class="badges" aria-label="Trust and accreditation">
<span class="badge">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="m9 16.2-3.5-3.5L4 14.2 9 19l11-11-1.4-1.4z"/></svg>
Registered Charity #84-2210
</span>
<span class="badge">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path fill="currentColor" d="M12 1 3 5v6c0 5 3.8 9.4 9 11 5.2-1.6 9-6 9-11V5l-9-4z"/></svg>
Independently audited
</span>
<span class="badge">Tax-deductible 501(c)(3)</span>
</div>
<a class="btn btn--donate" href="#donate" id="donateBtn">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path fill="currentColor" d="M12 21s-7.5-4.6-10-9.3C.2 8 2.2 4.5 5.7 4.5c2 0 3.4 1.1 4.3 2.5.9-1.4 2.3-2.5 4.3-2.5 3.5 0 5.5 3.5 3.7 7.2C19.5 16.4 12 21 12 21z"/></svg>
Donate & add to these numbers
</a>
</footer>
</section>
</main>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Impact Stat Counter
A transparency-first stat block for charity and nonprofit sites. Six cards form a responsive grid — each one a large, eased count-up number with an icon, a plain-language label, and a short context line (“248,930 hot meals served across 38 community kitchens”). The warm sand background, humanist Fraunces headings, and mission-teal accents keep the tone hopeful rather than corporate, while a registered-charity badge row and a donate call to action reinforce trust at a glance.
The counters do not run on load. An IntersectionObserver watches the grid and only fires when it
scrolls into view, at which point the cards stagger into place and each figure animates from zero
using an ease-out cubic curve. Numbers are formatted on the fly with thousands separators, optional
decimals, and prefix/suffix support, so a single component handles 3.1M, $92¢, and 14 alike.
Every interaction is vanilla JS with no dependencies. A replay button restarts the whole sequence,
the cards are keyboard-focusable with descriptive aria-labels, the donate button surfaces a toast,
and prefers-reduced-motion is honored by snapping straight to the final values.
Illustrative UI only — fictional organization, not a real charity or donation system.