Open demo

CSS code

HTML
<div><span class="lbl">Numbers with short stories</span>
<section class="im im-stories" aria-labelledby="im-stories-title">
  <h3 id="im-stories-title">What your gifts did in 2026</h3>
  <p>Wellspring Trust drills and repairs wells with village committees in eastern Kenya. These are this year's figures, checked by our field team in September.</p>
  <ul>
    <li><p class="im-num"><span data-count>186</span></p><h4>wells drilled and still working</h4>
      <p>Every well has a village committee and a trained mechanic. 181 of them kept pumping through the dry season.</p><a href="#">See the well map</a></li>
    <li><p class="im-num"><span data-count>2.4</span><span class="im-unit">M litres</span></p><h4>of clean water drawn every day</h4>
      <p>Enough for drinking, cooking and washing for about 120,000 people, read from flow meters on 64 pumps.</p><a href="#">How we measure</a></li>
    <li><p class="im-num"><span data-count>41,000</span></p><h4>people within 15 minutes' walk of water</h4>
      <p>Amara Wanjiru used to walk three hours a day for water. She now chairs the committee for the well in Kyamatu.</p><a href="#">Read Amara's story</a></li>
  </ul>
</section></div>

<div><span class="lbl">Milestone rail</span>
<ol class="im im-rail" aria-label="Wellspring Trust milestones">
  <li><time datetime="2014">2014</time><p class="im-num"><span data-count>1</span></p><p>first well, in Kitui County</p></li>
  <li><time datetime="2017">2017</time><p class="im-num"><span data-count>50</span></p><p>wells, plus rain tanks at 12 schools</p></li>
  <li><time datetime="2020">2020</time><p class="im-num"><span data-count>100</span></p><p>wells and 30 trained village mechanics</p></li>
  <li><time datetime="2023">2023</time><p class="im-num"><span data-count>150</span></p><p>wells, the first with solar pumps</p></li>
  <li><time datetime="2026">2026</time><p class="im-num"><span data-count>186</span></p><p>wells and counting</p></li>
</ol></div>

<div><span class="lbl">Headline figure with a breakdown</span>
<section class="im im-feature" aria-labelledby="im-feature-title">
  <div><p class="im-num"><span data-count>92</span><span class="im-unit">p</span></p>
    <h3 id="im-feature-title">of every £1 you give goes straight to wells, repairs and training</h3></div>
  <figure><blockquote><p>We used to count the jerrycans. Now we count the children who are in class by eight.</p></blockquote>
    <figcaption>Grace Mutua, head teacher, Kyamatu Primary School</figcaption></figure>
  <ul class="im-split" aria-label="Where the rest goes">
    <li><strong>5p</strong> fundraising and supporter care</li>
    <li><strong>3p</strong> audit, insurance and admin</li>
    <li><strong>£1.84M</strong> raised from 11,200 donors in 2026</li>
  </ul>
</section></div>
CSS
/* Impact and milestone sections for a nonprofit. Big serif numbers count up on view (see the script);
   the numbers in the HTML are the real figures, so they read correctly without JavaScript. */
.im{--im-ink:#1f2a24;--im-muted:#4f5a53;--im-accent:#0f5e55;--im-warm:#a3471f;--im-line:#e6dfd2;--im-paper:#fffdf8;
  --im-serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;color:var(--im-ink)}
.im-num{font-family:var(--im-serif);font-weight:700;line-height:1;letter-spacing:-.02em;font-variant-numeric:lining-nums tabular-nums;white-space:nowrap}
.im-unit{font-size:.5em;margin-left:.12em;color:var(--im-warm)}
.im a{color:var(--im-accent);font-weight:700;text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px;border-radius:3px}
.im a:hover{text-decoration-thickness:2px}
.im a:focus-visible{outline:2px solid var(--im-accent);outline-offset:3px}

/* 1. Numbers with short stories */
.im-stories{background:var(--im-paper);border:1px solid var(--im-line);border-radius:24px;padding:clamp(24px,4vw,44px)}
.im-stories h3{font-family:var(--im-serif);font-size:clamp(1.5rem,3vw,2rem);font-weight:700;line-height:1.2;max-width:24ch}
.im-stories > p{margin-top:8px;color:var(--im-muted);max-width:60ch}
.im-stories ul{list-style:none;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:30px;margin-top:30px}
.im-stories li{border-top:2px solid var(--im-ink);padding-top:18px;display:flex;flex-direction:column;gap:8px}
.im-stories .im-num{font-size:clamp(3rem,6vw,4.2rem)}
.im-stories h4{font-size:1rem;line-height:1.35}
.im-stories li p:not(.im-num){color:var(--im-muted);font-size:.94rem}
.im-stories li a{margin-top:auto;width:fit-content}

/* 2. Milestone rail: a horizontal timeline that turns vertical on phones */
.im-rail{list-style:none;display:grid;grid-template-columns:repeat(5,minmax(0,1fr));position:relative}
.im-rail::before{content:"";position:absolute;left:10%;right:10%;top:9px;height:2px;background:linear-gradient(90deg,var(--im-line),var(--im-accent))}
.im-rail li{position:relative;text-align:center;padding:34px 10px 0}
.im-rail li::before{content:"";position:absolute;top:2px;left:50%;width:16px;height:16px;margin-left:-8px;border-radius:50%;
  background:var(--im-paper);border:3px solid var(--im-accent)}
.im-rail li:last-child::before{background:var(--im-accent);box-shadow:0 0 0 5px rgba(15,94,85,.18)}
.im-rail time{display:block;font-size:.8rem;font-weight:700;letter-spacing:.1em;color:var(--im-warm)}
.im-rail .im-num{font-size:clamp(2.2rem,4vw,2.9rem);margin:6px 0 4px}
.im-rail p:last-child{font-size:.88rem;color:var(--im-muted);max-width:20ch;margin:0 auto}

/* 3. One headline figure with a breakdown */
.im-feature{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,1fr);gap:28px 48px;align-items:center;
  background:var(--im-accent);color:#f4fbf8;border-radius:28px;padding:clamp(26px,4.5vw,52px)}
.im-feature .im-num{font-size:clamp(5rem,13vw,8.5rem);color:#fff}
.im-feature .im-unit{color:#ffcfa8}
.im-feature h3{margin-top:10px;font-size:1.2rem;line-height:1.35;max-width:26ch}
.im-feature blockquote{font-family:var(--im-serif);font-size:1.12rem;line-height:1.5;border-left:3px solid #ffcfa8;padding-left:18px}
.im-feature figcaption{margin-top:10px;padding-left:21px;font-size:.86rem;color:#cde8e2}
.im-split{grid-column:1/-1;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;list-style:none;border-top:1px solid rgba(255,255,255,.25);padding-top:22px}
.im-split li{font-size:.88rem;color:#cde8e2}
.im-split strong{display:block;font-family:var(--im-serif);font-size:1.7rem;color:#fff;font-variant-numeric:lining-nums tabular-nums}

@media (max-width:820px){.im-stories ul{grid-template-columns:1fr}.im-feature{grid-template-columns:1fr}}
@media (max-width:720px){
  .im-rail{grid-template-columns:1fr;gap:20px;padding-left:34px}
  .im-rail::before{left:9px;right:auto;top:8px;bottom:8px;width:2px;height:auto;background:linear-gradient(var(--im-line),var(--im-accent))}
  .im-rail li{text-align:left;padding:0}
  .im-rail li::before{left:-34px;margin-left:0}
  .im-rail p:last-child{margin:0;max-width:none}
}
@media (max-width:480px){.im-split{grid-template-columns:1fr}}
@media (prefers-reduced-motion:reduce){.im *{transition:none;animation:none}}
JavaScript
// Count-up on view. The text inside each [data-count] element is the real, final number, so it shows as-is
// without JavaScript and under prefers-reduced-motion (no count from 0). While the digits run, an invisible copy
// of the final number keeps the width steady (digits grow from the right, next to any unit) and a visually
// hidden copy is what screen readers get.
// Optional: data-from="…" (start value, default 0) and data-duration="…" (ms, default 1800).
(() => {
  const els = document.querySelectorAll('[data-count]');
  if (!els.length || !('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  const lang = document.documentElement.lang || undefined;
  const sep = type => new Intl.NumberFormat(lang).formatToParts(12345.6).find(p => p.type === type)?.value;
  const group = sep('group') || ',', point = sep('decimal') || '.';
  const esc = s => s.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
  const numRe = new RegExp(`^(\\D*?)(\\d[\\d${esc(group)}]*(?:${esc(point)}\\d+)?)(.*)$`, 's');
  const ease = t => 1 - (1 - t) ** 4;
  const span = (css, text) => { const s = document.createElement('span'); s.style.cssText = css; s.textContent = text; return s; };
  const jobs = new WeakMap();
  const run = el => {
    const j = jobs.get(el), t0 = performance.now();
    const tick = now => {
      const p = Math.min((now - t0) / j.dur, 1);
      if (p < 1) { j.live.textContent = j.pre + j.fmt.format(j.from + (j.to - j.from) * ease(p)) + j.post; requestAnimationFrame(tick); }
      else { el.textContent = j.final; el.style.display = ''; }
    };
    requestAnimationFrame(tick);
  };
  const io = new IntersectionObserver(list => list.forEach(e => {
    if (e.isIntersecting) { io.unobserve(e.target); run(e.target); }
  }), { threshold: 0.6 });
  els.forEach(el => {
    const final = el.textContent, m = final.trim().match(numRe);
    if (!m) return;
    const [, pre, num, post] = m, dec = num.includes(point) ? num.split(point)[1].length : 0;
    const to = parseFloat(num.split(group).join('').replace(point, '.')), from = parseFloat(el.dataset.from) || 0;
    const fmt = new Intl.NumberFormat(lang, { minimumFractionDigits: dec, maximumFractionDigits: dec, useGrouping: num.includes(group) });
    const live = span('grid-area:1/1;justify-self:end', pre + fmt.format(from) + post);
    live.setAttribute('aria-hidden', 'true');
    el.style.display = 'inline-grid';
    el.replaceChildren(span('grid-area:1/1;visibility:hidden', final.trim()), live,
      span('position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap', final.trim()));
    jobs.set(el, { final, pre, post, to, from, fmt, live, dur: +el.dataset.duration || 1800 });
    io.observe(el);
  });
})();

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Numbers with short stories</span>
<section class="im im-stories card" aria-labelledby="im-stories-title"><div class="card-body" style="padding:clamp(24px,4vw,44px)">
  <h3 class="im-serif fw-bold lh-sm mb-2" id="im-stories-title" style="font-size:clamp(1.5rem,3vw,2rem);max-width:24ch">What your gifts did in 2026</h3>
  <p class="text-body-secondary" style="max-width:60ch">Wellspring Trust drills and repairs wells with village committees in eastern Kenya. These are this year's figures, checked by our field team in September.</p>
  <ul class="list-unstyled row row-cols-1 row-cols-lg-3 g-4 mt-2 mb-0">
    <li class="col"><div class="im-story h-100 d-flex flex-column gap-2"><p class="im-num"><span data-count>186</span></p><h4 class="fs-6 fw-bold mb-0">wells drilled and still working</h4>
      <p class="text-body-secondary" style="font-size:.94rem">Every well has a village committee and a trained mechanic. 181 of them kept pumping through the dry season.</p><a class="mt-auto align-self-start" href="#">See the well map</a></div></li>
    <li class="col"><div class="im-story h-100 d-flex flex-column gap-2"><p class="im-num"><span data-count>2.4</span><span class="im-unit">M litres</span></p><h4 class="fs-6 fw-bold mb-0">of clean water drawn every day</h4>
      <p class="text-body-secondary" style="font-size:.94rem">Enough for drinking, cooking and washing for about 120,000 people, read from flow meters on 64 pumps.</p><a class="mt-auto align-self-start" href="#">How we measure</a></div></li>
    <li class="col"><div class="im-story h-100 d-flex flex-column gap-2"><p class="im-num"><span data-count>41,000</span></p><h4 class="fs-6 fw-bold mb-0">people within 15 minutes' walk of water</h4>
      <p class="text-body-secondary" style="font-size:.94rem">Amara Wanjiru used to walk three hours a day for water. She now chairs the committee for the well in Kyamatu.</p><a class="mt-auto align-self-start" href="#">Read Amara's story</a></div></li>
  </ul>
</div></section></div>

<div><span class="lbl">Milestone rail</span>
<ol class="im im-rail list-unstyled row row-cols-1 row-cols-md-5 gx-0 gy-4 gy-md-0 text-center mb-0 mx-0" aria-label="Wellspring Trust milestones">
  <li class="col"><time datetime="2014">2014</time><p class="im-num"><span data-count>1</span></p><p class="text-body-secondary">first well, in Kitui County</p></li>
  <li class="col"><time datetime="2017">2017</time><p class="im-num"><span data-count>50</span></p><p class="text-body-secondary">wells, plus rain tanks at 12 schools</p></li>
  <li class="col"><time datetime="2020">2020</time><p class="im-num"><span data-count>100</span></p><p class="text-body-secondary">wells and 30 trained village mechanics</p></li>
  <li class="col"><time datetime="2023">2023</time><p class="im-num"><span data-count>150</span></p><p class="text-body-secondary">wells, the first with solar pumps</p></li>
  <li class="col"><time datetime="2026">2026</time><p class="im-num"><span data-count>186</span></p><p class="text-body-secondary">wells and counting</p></li>
</ol></div>

<div><span class="lbl">Headline figure with a breakdown</span>
<section class="im im-feature" aria-labelledby="im-feature-title"><div class="row g-4 gx-lg-5 align-items-center">
  <div class="col-lg-6"><p class="im-num"><span data-count>92</span><span class="im-unit">p</span></p>
    <h3 class="fs-5 fw-bold lh-base mt-2 mb-0" id="im-feature-title" style="max-width:26ch">of every £1 you give goes straight to wells, repairs and training</h3></div>
  <div class="col-lg-6"><figure class="mb-0"><blockquote class="mb-0"><p>We used to count the jerrycans. Now we count the children who are in class by eight.</p></blockquote>
    <figcaption class="mt-2">Grace Mutua, head teacher, Kyamatu Primary School</figcaption></figure></div>
  <div class="col-12"><ul class="im-split list-unstyled row row-cols-1 row-cols-sm-3 g-3 pt-3 mb-0" aria-label="Where the rest goes">
    <li class="col"><strong>5p</strong> fundraising and supporter care</li>
    <li class="col"><strong>3p</strong> audit, insurance and admin</li>
    <li class="col"><strong>£1.84M</strong> raised from 11,200 donors in 2026</li>
  </ul></div>
</div></section></div>
CSS
/* Impact sections on Bootstrap: grid rows, .card and utilities, with a serif face for the figures.
   The timeline rail and the colour band need the few rules below. */
.im{--im-ink:#1f2a24;--im-accent:#0f5e55;--im-warm:#a3471f;--im-line:#e6dfd2;--im-paper:#fffdf8;--bs-secondary-color:#4f5a53;
  --im-serif:"Iowan Old Style","Palatino Linotype",Palatino,Georgia,serif;color:var(--im-ink)}
.im p{margin:0}
.im-serif{font-family:var(--im-serif)}
.im-num{font-family:var(--im-serif);font-weight:700;line-height:1;letter-spacing:-.02em;font-variant-numeric:lining-nums tabular-nums;white-space:nowrap}
.im-unit{font-size:.5em;margin-left:.12em;color:var(--im-warm)}
.im a{color:var(--im-accent);font-weight:700;text-decoration-thickness:1px;text-underline-offset:3px;border-radius:3px}
.im a:hover{text-decoration-thickness:2px}
.im a:focus-visible{outline:2px solid var(--im-accent);outline-offset:3px;box-shadow:none}
.im.card{--bs-card-bg:var(--im-paper);--bs-card-border-color:var(--im-line);--bs-card-border-radius:24px}
.im .im-story{border-top:2px solid var(--im-ink);padding-top:18px}
.im-stories .im-num{font-size:clamp(3rem,6vw,4.2rem)}
.im-rail{position:relative}
.im-rail::before{content:"";position:absolute;left:10%;right:10%;top:9px;height:2px;background:linear-gradient(90deg,var(--im-line),var(--im-accent))}
.im-rail li{position:relative;padding:34px 10px 0}
.im-rail li::before{content:"";position:absolute;top:2px;left:50%;width:16px;height:16px;margin-left:-8px;border-radius:50%;background:var(--im-paper);border:3px solid var(--im-accent)}
.im-rail li:last-child::before{background:var(--im-accent);box-shadow:0 0 0 5px rgba(15,94,85,.18)}
.im-rail time{display:block;font-size:.8rem;font-weight:700;letter-spacing:.1em;color:var(--im-warm)}
.im-rail .im-num{font-size:clamp(2.2rem,4vw,2.9rem);margin:6px 0 4px}
.im-rail p:last-child{font-size:.88rem;max-width:20ch;margin:0 auto}
.im-feature{background:var(--im-accent);color:#f4fbf8;border-radius:28px;padding:clamp(26px,4.5vw,52px)}
.im-feature .im-num{font-size:clamp(5rem,13vw,8.5rem);color:#fff}
.im-feature .im-unit{color:#ffcfa8}
.im-feature blockquote{font-family:var(--im-serif);font-size:1.12rem;line-height:1.5;border-left:3px solid #ffcfa8;padding-left:18px}
.im-feature figcaption{padding-left:21px;font-size:.86rem;color:#cde8e2}
.im-feature .im-split{border-top:1px solid rgba(255,255,255,.25);font-size:.88rem;color:#cde8e2}
.im-feature .im-split strong{display:block;font-family:var(--im-serif);font-size:1.7rem;color:#fff;font-variant-numeric:lining-nums tabular-nums}
@media (max-width:767.98px){
  .im-rail{padding-left:34px!important}
  .im-rail::before{left:9px;right:auto;top:8px;bottom:8px;width:2px;height:auto;background:linear-gradient(var(--im-line),var(--im-accent))}
  .im-rail li{text-align:left!important;padding:0}
  .im-rail li::before{left:-34px;margin-left:0}
  .im-rail p:last-child{margin:0;max-width:none}
}
@media (prefers-reduced-motion:reduce){.im *{transition:none;animation:none}}
JavaScript
// Count-up on view. The text inside each [data-count] element is the real, final number, so it shows as-is
// without JavaScript and under prefers-reduced-motion (no count from 0). While the digits run, an invisible copy
// of the final number keeps the width steady (digits grow from the right, next to any unit) and a visually
// hidden copy is what screen readers get.
// Optional: data-from="…" (start value, default 0) and data-duration="…" (ms, default 1800).
(() => {
  const els = document.querySelectorAll('[data-count]');
  if (!els.length || !('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  const lang = document.documentElement.lang || undefined;
  const sep = type => new Intl.NumberFormat(lang).formatToParts(12345.6).find(p => p.type === type)?.value;
  const group = sep('group') || ',', point = sep('decimal') || '.';
  const esc = s => s.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
  const numRe = new RegExp(`^(\\D*?)(\\d[\\d${esc(group)}]*(?:${esc(point)}\\d+)?)(.*)$`, 's');
  const ease = t => 1 - (1 - t) ** 4;
  const span = (css, text) => { const s = document.createElement('span'); s.style.cssText = css; s.textContent = text; return s; };
  const jobs = new WeakMap();
  const run = el => {
    const j = jobs.get(el), t0 = performance.now();
    const tick = now => {
      const p = Math.min((now - t0) / j.dur, 1);
      if (p < 1) { j.live.textContent = j.pre + j.fmt.format(j.from + (j.to - j.from) * ease(p)) + j.post; requestAnimationFrame(tick); }
      else { el.textContent = j.final; el.style.display = ''; }
    };
    requestAnimationFrame(tick);
  };
  const io = new IntersectionObserver(list => list.forEach(e => {
    if (e.isIntersecting) { io.unobserve(e.target); run(e.target); }
  }), { threshold: 0.6 });
  els.forEach(el => {
    const final = el.textContent, m = final.trim().match(numRe);
    if (!m) return;
    const [, pre, num, post] = m, dec = num.includes(point) ? num.split(point)[1].length : 0;
    const to = parseFloat(num.split(group).join('').replace(point, '.')), from = parseFloat(el.dataset.from) || 0;
    const fmt = new Intl.NumberFormat(lang, { minimumFractionDigits: dec, maximumFractionDigits: dec, useGrouping: num.includes(group) });
    const live = span('grid-area:1/1;justify-self:end', pre + fmt.format(from) + post);
    live.setAttribute('aria-hidden', 'true');
    el.style.display = 'inline-grid';
    el.replaceChildren(span('grid-area:1/1;visibility:hidden', final.trim()), live,
      span('position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap', final.trim()));
    jobs.set(el, { final, pre, post, to, from, fmt, live, dur: +el.dataset.duration || 1800 });
    io.observe(el);
  });
})();

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5c5a52] uppercase">Numbers with short stories</span>
<section class="rounded-3xl border border-im-line bg-im-paper p-[clamp(24px,4vw,44px)]" aria-labelledby="im-stories-title">
  <h3 class="max-w-[24ch] font-im-serif text-[clamp(1.5rem,3vw,2rem)] leading-[1.2] font-bold" id="im-stories-title">What your gifts did in 2026</h3>
  <p class="mt-2 max-w-[60ch] text-im-muted">Wellspring Trust drills and repairs wells with village committees in eastern Kenya. These are this year's figures, checked by our field team in September.</p>
  <ul class="mt-[30px] grid grid-cols-1 gap-[30px] min-[821px]:grid-cols-3">
    <li class="flex flex-col gap-2 border-t-2 border-im-ink pt-[18px]"><p class="font-im-serif text-[clamp(3rem,6vw,4.2rem)] leading-none font-bold tracking-[-.02em] whitespace-nowrap tabular-nums lining-nums"><span data-count>186</span></p><h4 class="leading-[1.35] font-bold">wells drilled and still working</h4>
      <p class="text-[.94rem] text-im-muted">Every well has a village committee and a trained mechanic. 181 of them kept pumping through the dry season.</p><a class="mt-auto w-fit rounded-[3px] font-bold text-im-accent underline decoration-1 underline-offset-[3px] hover:decoration-2 focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-im-accent" href="#">See the well map</a></li>
    <li class="flex flex-col gap-2 border-t-2 border-im-ink pt-[18px]"><p class="font-im-serif text-[clamp(3rem,6vw,4.2rem)] leading-none font-bold tracking-[-.02em] whitespace-nowrap tabular-nums lining-nums"><span data-count>2.4</span><span class="ml-[.12em] text-[.5em] text-im-warm">M litres</span></p><h4 class="leading-[1.35] font-bold">of clean water drawn every day</h4>
      <p class="text-[.94rem] text-im-muted">Enough for drinking, cooking and washing for about 120,000 people, read from flow meters on 64 pumps.</p><a class="mt-auto w-fit rounded-[3px] font-bold text-im-accent underline decoration-1 underline-offset-[3px] hover:decoration-2 focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-im-accent" href="#">How we measure</a></li>
    <li class="flex flex-col gap-2 border-t-2 border-im-ink pt-[18px]"><p class="font-im-serif text-[clamp(3rem,6vw,4.2rem)] leading-none font-bold tracking-[-.02em] whitespace-nowrap tabular-nums lining-nums"><span data-count>41,000</span></p><h4 class="leading-[1.35] font-bold">people within 15 minutes' walk of water</h4>
      <p class="text-[.94rem] text-im-muted">Amara Wanjiru used to walk three hours a day for water. She now chairs the committee for the well in Kyamatu.</p><a class="mt-auto w-fit rounded-[3px] font-bold text-im-accent underline decoration-1 underline-offset-[3px] hover:decoration-2 focus-visible:outline-2 focus-visible:outline-offset-[3px] focus-visible:outline-im-accent" href="#">Read Amara's story</a></li>
  </ul>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5c5a52] uppercase">Milestone rail</span>
<ol class="relative grid grid-cols-1 gap-5 pl-[34px] before:absolute before:top-2 before:bottom-2 before:left-[9px] before:w-0.5 before:bg-linear-to-b before:from-im-line before:to-im-accent min-[721px]:grid-cols-5 min-[721px]:gap-0 min-[721px]:pl-0 min-[721px]:before:inset-x-[10%] min-[721px]:before:top-[9px] min-[721px]:before:bottom-auto min-[721px]:before:h-0.5 min-[721px]:before:w-auto min-[721px]:before:bg-linear-to-r" aria-label="Wellspring Trust milestones">
  <li class="relative before:absolute before:top-0.5 before:-left-[34px] before:size-4 before:rounded-full before:border-[3px] before:border-im-accent before:bg-im-paper min-[721px]:px-2.5 min-[721px]:pt-[34px] min-[721px]:text-center min-[721px]:before:left-1/2 min-[721px]:before:-ml-2"><time class="block text-[.8rem] font-bold tracking-[.1em] text-im-warm" datetime="2014">2014</time><p class="my-1.5 font-im-serif text-[clamp(2.2rem,4vw,2.9rem)] leading-none font-bold tracking-[-.02em] whitespace-nowrap tabular-nums lining-nums"><span data-count>1</span></p><p class="text-[.88rem] text-im-muted min-[721px]:mx-auto min-[721px]:max-w-[20ch]">first well, in Kitui County</p></li>
  <li class="relative before:absolute before:top-0.5 before:-left-[34px] before:size-4 before:rounded-full before:border-[3px] before:border-im-accent before:bg-im-paper min-[721px]:px-2.5 min-[721px]:pt-[34px] min-[721px]:text-center min-[721px]:before:left-1/2 min-[721px]:before:-ml-2"><time class="block text-[.8rem] font-bold tracking-[.1em] text-im-warm" datetime="2017">2017</time><p class="my-1.5 font-im-serif text-[clamp(2.2rem,4vw,2.9rem)] leading-none font-bold tracking-[-.02em] whitespace-nowrap tabular-nums lining-nums"><span data-count>50</span></p><p class="text-[.88rem] text-im-muted min-[721px]:mx-auto min-[721px]:max-w-[20ch]">wells, plus rain tanks at 12 schools</p></li>
  <li class="relative before:absolute before:top-0.5 before:-left-[34px] before:size-4 before:rounded-full before:border-[3px] before:border-im-accent before:bg-im-paper min-[721px]:px-2.5 min-[721px]:pt-[34px] min-[721px]:text-center min-[721px]:before:left-1/2 min-[721px]:before:-ml-2"><time class="block text-[.8rem] font-bold tracking-[.1em] text-im-warm" datetime="2020">2020</time><p class="my-1.5 font-im-serif text-[clamp(2.2rem,4vw,2.9rem)] leading-none font-bold tracking-[-.02em] whitespace-nowrap tabular-nums lining-nums"><span data-count>100</span></p><p class="text-[.88rem] text-im-muted min-[721px]:mx-auto min-[721px]:max-w-[20ch]">wells and 30 trained village mechanics</p></li>
  <li class="relative before:absolute before:top-0.5 before:-left-[34px] before:size-4 before:rounded-full before:border-[3px] before:border-im-accent before:bg-im-paper min-[721px]:px-2.5 min-[721px]:pt-[34px] min-[721px]:text-center min-[721px]:before:left-1/2 min-[721px]:before:-ml-2"><time class="block text-[.8rem] font-bold tracking-[.1em] text-im-warm" datetime="2023">2023</time><p class="my-1.5 font-im-serif text-[clamp(2.2rem,4vw,2.9rem)] leading-none font-bold tracking-[-.02em] whitespace-nowrap tabular-nums lining-nums"><span data-count>150</span></p><p class="text-[.88rem] text-im-muted min-[721px]:mx-auto min-[721px]:max-w-[20ch]">wells, the first with solar pumps</p></li>
  <li class="relative before:absolute before:top-0.5 before:-left-[34px] before:size-4 before:rounded-full before:bg-im-accent before:shadow-[0_0_0_5px_rgba(15,94,85,.18)] min-[721px]:px-2.5 min-[721px]:pt-[34px] min-[721px]:text-center min-[721px]:before:left-1/2 min-[721px]:before:-ml-2"><time class="block text-[.8rem] font-bold tracking-[.1em] text-im-warm" datetime="2026">2026</time><p class="my-1.5 font-im-serif text-[clamp(2.2rem,4vw,2.9rem)] leading-none font-bold tracking-[-.02em] whitespace-nowrap tabular-nums lining-nums"><span data-count>186</span></p><p class="text-[.88rem] text-im-muted min-[721px]:mx-auto min-[721px]:max-w-[20ch]">wells and counting</p></li>
</ol></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#5c5a52] uppercase">Headline figure with a breakdown</span>
<section class="grid grid-cols-1 items-center gap-x-12 gap-y-7 rounded-[28px] bg-im-accent p-[clamp(26px,4.5vw,52px)] text-[#f4fbf8] min-[821px]:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)]" aria-labelledby="im-feature-title">
  <div><p class="font-im-serif text-[clamp(5rem,13vw,8.5rem)] leading-none font-bold tracking-[-.02em] whitespace-nowrap text-white tabular-nums lining-nums"><span data-count>92</span><span class="ml-[.12em] text-[.5em] text-im-peach">p</span></p>
    <h3 class="mt-2.5 max-w-[26ch] text-[1.2rem] leading-[1.35] font-bold" id="im-feature-title">of every £1 you give goes straight to wells, repairs and training</h3></div>
  <figure><blockquote class="border-l-[3px] border-im-peach pl-[18px] font-im-serif text-[1.12rem] leading-normal"><p>We used to count the jerrycans. Now we count the children who are in class by eight.</p></blockquote>
    <figcaption class="mt-2.5 pl-[21px] text-[.86rem] text-im-mint">Grace Mutua, head teacher, Kyamatu Primary School</figcaption></figure>
  <ul class="col-span-full grid grid-cols-1 gap-4 border-t border-white/25 pt-[22px] min-[481px]:grid-cols-3" aria-label="Where the rest goes">
    <li class="text-[.88rem] text-im-mint"><strong class="block font-im-serif text-[1.7rem] text-white tabular-nums lining-nums">5p</strong> fundraising and supporter care</li>
    <li class="text-[.88rem] text-im-mint"><strong class="block font-im-serif text-[1.7rem] text-white tabular-nums lining-nums">3p</strong> audit, insurance and admin</li>
    <li class="text-[.88rem] text-im-mint"><strong class="block font-im-serif text-[1.7rem] text-white tabular-nums lining-nums">£1.84M</strong> raised from 11,200 donors in 2026</li>
  </ul>
</section></div>
CSS
/* Impact sections: brand colours and the serif face for the figures as theme tokens. */
@theme {
  --color-im-ink: #1f2a24; --color-im-muted: #4f5a53; --color-im-accent: #0f5e55; --color-im-warm: #a3471f;
  --color-im-line: #e6dfd2; --color-im-paper: #fffdf8; --color-im-peach: #ffcfa8; --color-im-mint: #cde8e2;
  --font-im-serif: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
}
JavaScript
// Count-up on view. The text inside each [data-count] element is the real, final number, so it shows as-is
// without JavaScript and under prefers-reduced-motion (no count from 0). While the digits run, an invisible copy
// of the final number keeps the width steady (digits grow from the right, next to any unit) and a visually
// hidden copy is what screen readers get.
// Optional: data-from="…" (start value, default 0) and data-duration="…" (ms, default 1800).
(() => {
  const els = document.querySelectorAll('[data-count]');
  if (!els.length || !('IntersectionObserver' in window) || matchMedia('(prefers-reduced-motion: reduce)').matches) return;
  const lang = document.documentElement.lang || undefined;
  const sep = type => new Intl.NumberFormat(lang).formatToParts(12345.6).find(p => p.type === type)?.value;
  const group = sep('group') || ',', point = sep('decimal') || '.';
  const esc = s => s.replace(/[\\^$.*+?()[\]{}|]/g, '\\$&');
  const numRe = new RegExp(`^(\\D*?)(\\d[\\d${esc(group)}]*(?:${esc(point)}\\d+)?)(.*)$`, 's');
  const ease = t => 1 - (1 - t) ** 4;
  const span = (css, text) => { const s = document.createElement('span'); s.style.cssText = css; s.textContent = text; return s; };
  const jobs = new WeakMap();
  const run = el => {
    const j = jobs.get(el), t0 = performance.now();
    const tick = now => {
      const p = Math.min((now - t0) / j.dur, 1);
      if (p < 1) { j.live.textContent = j.pre + j.fmt.format(j.from + (j.to - j.from) * ease(p)) + j.post; requestAnimationFrame(tick); }
      else { el.textContent = j.final; el.style.display = ''; }
    };
    requestAnimationFrame(tick);
  };
  const io = new IntersectionObserver(list => list.forEach(e => {
    if (e.isIntersecting) { io.unobserve(e.target); run(e.target); }
  }), { threshold: 0.6 });
  els.forEach(el => {
    const final = el.textContent, m = final.trim().match(numRe);
    if (!m) return;
    const [, pre, num, post] = m, dec = num.includes(point) ? num.split(point)[1].length : 0;
    const to = parseFloat(num.split(group).join('').replace(point, '.')), from = parseFloat(el.dataset.from) || 0;
    const fmt = new Intl.NumberFormat(lang, { minimumFractionDigits: dec, maximumFractionDigits: dec, useGrouping: num.includes(group) });
    const live = span('grid-area:1/1;justify-self:end', pre + fmt.format(from) + post);
    live.setAttribute('aria-hidden', 'true');
    el.style.display = 'inline-grid';
    el.replaceChildren(span('grid-area:1/1;visibility:hidden', final.trim()), live,
      span('position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap', final.trim()));
    jobs.set(el, { final, pre, post, to, from, fmt, live, dur: +el.dataset.duration || 1800 });
    io.observe(el);
  });
})();

About this set

Big numbers with the story behind them, for nonprofits, annual reports and about pages. Numbers with short stories gives three figures a heading, two sentences about one person or place, and a link to read more. Milestone rail sets five years along a horizontal timeline with the count at each one, and turns into a vertical timeline on phones. Headline figure with a breakdown puts one very large number, 92p of every pound, beside a quote, with a short breakdown of where the rest goes. The figures use a serif face and count up when they scroll into view, using the same small script as the count-up set: the real number is the text in the HTML, the script reads its digits and decimals and counts to it with requestAnimationFrame and Intl.NumberFormat. Use the stories layout when each number has a person behind it, the rail to show growth over time, and the headline figure when a single fact should carry the section.

What’s included

  • Serif headline numbers with short stories and links
  • Five-step milestone rail that turns vertical on phones
  • Single headline figure with quote and breakdown
  • Count-up on view with the real figure written in the HTML
  • No count under prefers-reduced-motion

Accessibility

Each section has a heading or label, the milestone rail is an ordered list with time elements, and the quote is a figure with a figcaption naming the speaker. While a number counts, screen readers get a hidden copy of the final figure, and with prefers-reduced-motion the numbers are shown at their final values. Links are underlined and show a focus outline, and all text meets WCAG AA contrast.

Customise it

Change --im-accent, --im-warm and --im-paper on .im for the colours and --im-serif for the face of the figures. In Bootstrap the same variables sit on .im; in Tailwind edit the --color-im-* theme colours and --font-im-serif.