Impact Stats
A muted statement above a four-column band of figures that count up as the section arrives, each ruled by a hairline that draws itself down. Folds to a 2×2 divided by a single cross.
- Stats
- No dependencies
- MIT
- Google Sans Flex · SIL Open Font License 1.1
Copy the code
The whole component, in one file. Yours from the moment you paste it.
Install it
Straight into a project with a shadcn setup — no account, no limit.
npx shadcn@latest add https://designblocs.com/r/impact-stats.json Hand it to an agent
A prompt for Claude Code, Cursor or v0 that adapts the component to your codebase rather than pasting it in raw.
src/components/ui-library/blocks/ImpactStats.tsx
/**
* ImpactStats — a statement, then four figures that count up as they arrive.
*
* The figures are the point, so they are given the room: a muted statement on
* the left at the top, then a wide four-column band where each column is ruled
* by a hairline that draws itself downward, the number counts, and the caption
* rises in behind it. At narrow widths the band folds to a 2×2 divided by a
* single cross rather than four stacked rows, which keeps it a *set* of figures
* instead of a list.
*
* The count is deliberate rather than decorative: it starts when the section is
* actually looked at, runs once, and under `prefers-reduced-motion` the numbers
* are simply there. The accessible name of each figure is the final value from
* the first paint, so a screen reader never reads a number mid-count.
*
* SELF-CONTAINED: imports React and nothing else. Every style is either a
* complete Tailwind class or lives in the scoped block below.
*/
import { useEffect, useRef, useState } from "react";
export interface ImpactStat {
/** The final figure, written as it should read: "10,989,916". */
value: string;
/** The small label above the number. */
category: string;
/** The sentence under the number. */
label: string;
}
export interface ImpactStatsProps {
/** The opening clause, carried in full ink against the muted remainder. */
lead?: string;
statement?: string;
stats?: ImpactStat[];
tone?: "light" | "dark";
}
const DEFAULTS = {
lead: "Progress, measured.",
statement:
"A growing record of solutions delivered, lives reached, and water generated and preserved.",
stats: [
{ value: "252", category: "Solutions", label: "Solutions delivered" },
{ value: "10,989,916", category: "People reached", label: "Lives benefited to date" },
{ value: "21,749,220", category: "Water generated", label: "Litres of water generated to date" },
{ value: "86,996,880", category: "Water preserved", label: "Litres of water saved to date" },
] as ImpactStat[],
tone: "light" as const,
};
const STYLES = `
[data-block="ImpactStats"],
[data-block="ImpactStats"] :is(h2, p, span) {
font-family: "Google Sans Flex", "Inter Variable", Inter, ui-sans-serif, system-ui, -apple-system, sans-serif;
}
[data-block="ImpactStats"] {
--is-ink: #10171c;
--is-muted: rgb(16 23 28 / 48%);
--is-rule: rgb(16 23 28 / 22%);
--is-rule-soft: rgb(16 23 28 / 18%);
--is-ground: linear-gradient(180deg, #f3f8f8 0%, #fff 76%);
--is-gutter: 48px;
--is-ease: cubic-bezier(0.22, 1, 0.36, 1);
position: relative;
display: flex;
flex-direction: column;
isolation: isolate;
overflow: hidden;
padding: 134px var(--is-gutter) 138px;
color: var(--is-ink);
background: var(--is-ground);
}
[data-block="ImpactStats"][data-tone="dark"] {
--is-ink: #f4f8fa;
--is-muted: rgb(244 248 250 / 52%);
--is-rule: rgb(244 248 250 / 26%);
--is-rule-soft: rgb(244 248 250 / 18%);
--is-ground: linear-gradient(180deg, #0c1216 0%, #05080a 76%);
}
[data-block="ImpactStats"] .is-statement {
max-width: 31ch;
margin: 0;
color: var(--is-muted);
font-size: clamp(31px, 2.7vw, 50px);
font-weight: 400;
line-height: 1.04;
letter-spacing: -0.04em;
}
[data-block="ImpactStats"] .is-statement span {
color: var(--is-ink);
}
/* The band sits to the right of the statement at desk widths — the asymmetry
is what stops it reading as a generic four-up. */
[data-block="ImpactStats"] .is-stats {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
width: 70%;
min-height: 288px;
margin-top: 88px;
margin-left: auto;
}
[data-block="ImpactStats"] .is-stat {
position: relative;
display: flex;
min-width: 0;
flex-direction: column;
}
[data-block="ImpactStats"] .is-stat::before {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 1px;
background: var(--is-rule);
transform: scaleY(0);
transform-origin: top;
}
[data-block="ImpactStats"] .is-upper {
display: flex;
min-height: 0;
flex: 1;
flex-direction: column;
padding: 0 18px;
}
[data-block="ImpactStats"] .is-category {
max-width: 24ch;
margin: 0;
color: var(--is-muted);
font-size: 11px;
font-weight: 500;
line-height: 1.2;
letter-spacing: 0.02em;
text-transform: uppercase;
}
[data-block="ImpactStats"] .is-number {
margin: clamp(28px, 4vh, 46px) 0 0;
font-size: clamp(34px, 2.8vw, 54px);
font-weight: 400;
line-height: 0.9;
letter-spacing: -0.055em;
white-space: nowrap;
}
[data-block="ImpactStats"] .is-label {
max-width: 24ch;
margin: 0;
padding: 0 18px;
font-size: clamp(15px, 1.05vw, 19px);
line-height: 1.28;
letter-spacing: -0.02em;
opacity: 0;
transform: translateY(12px);
}
[data-block="ImpactStats"][data-visible="true"] .is-stat::before {
animation: is-rule-down 820ms var(--is-ease) both;
animation-delay: calc(var(--is-index) * 90ms);
}
[data-block="ImpactStats"][data-visible="true"] .is-label {
animation: is-label-in 460ms var(--is-ease) both;
animation-delay: calc(760ms + var(--is-index) * 90ms);
}
@keyframes is-rule-down { to { transform: scaleY(1); } }
@keyframes is-label-in { to { opacity: 1; transform: none; } }
@media (max-width: 1100px) {
[data-block="ImpactStats"] .is-stats {
grid-template-columns: repeat(2, minmax(0, 1fr));
width: 100%;
min-height: 0;
margin: 150px 0 0;
}
[data-block="ImpactStats"] .is-stat {
min-height: 270px;
border-bottom: 1px solid var(--is-rule-soft);
}
[data-block="ImpactStats"] .is-upper { padding: 24px 20px 0; }
[data-block="ImpactStats"] .is-label { padding: 0 20px 32px; }
[data-block="ImpactStats"] .is-stat::before { bottom: 32px; }
[data-block="ImpactStats"] .is-stat:nth-child(odd) .is-upper,
[data-block="ImpactStats"] .is-stat:nth-child(odd) .is-label { padding-left: 0; }
[data-block="ImpactStats"] .is-stat:nth-child(odd)::before { display: none; }
}
/* A 2×2 divided by one cross: a vertical rule between the columns, a
horizontal rule between the rows. Four stacked rows would lose the sense
that these figures belong together. */
@media (max-width: 767px) {
[data-block="ImpactStats"] {
--is-gutter: 20px;
padding-top: 96px;
}
[data-block="ImpactStats"] .is-statement { font-size: 34px; line-height: 1.06; }
[data-block="ImpactStats"] .is-stats { margin-top: 110px; }
[data-block="ImpactStats"] .is-stat { min-height: 0; border: 0; }
[data-block="ImpactStats"] .is-stat:nth-child(-n + 2) {
padding-bottom: 30px;
border-bottom: 1px solid var(--is-rule-soft);
}
[data-block="ImpactStats"] .is-stat:nth-child(n + 3) { padding-top: 30px; }
[data-block="ImpactStats"] .is-stat::before { display: block; bottom: 0; }
[data-block="ImpactStats"] .is-stat:nth-child(odd)::before { display: none; }
[data-block="ImpactStats"] .is-upper,
[data-block="ImpactStats"] .is-label { padding: 0; }
[data-block="ImpactStats"] .is-stat:nth-child(even) .is-upper,
[data-block="ImpactStats"] .is-stat:nth-child(even) .is-label { padding-left: 18px; }
[data-block="ImpactStats"] .is-stat:nth-child(odd) .is-upper,
[data-block="ImpactStats"] .is-stat:nth-child(odd) .is-label { padding-right: 18px; }
[data-block="ImpactStats"] .is-number {
margin-top: 18px;
font-size: clamp(19px, 6.1vw, 27px);
letter-spacing: -0.04em;
}
[data-block="ImpactStats"] .is-label { max-width: none; margin-top: 14px; font-size: 14px; }
[data-block="ImpactStats"] .is-category { max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
[data-block="ImpactStats"] .is-stat::before { transform: scaleY(1); animation: none !important; }
[data-block="ImpactStats"] .is-label { opacity: 1; transform: none; animation: none !important; }
}
`;
const FORMAT = new Intl.NumberFormat("en-US");
/**
* Count to the figure, once, when the section is looked at.
*
* The written value is the accessible name from the first paint — a screen
* reader should hear the number, not whatever it happened to be part way
* through — and with reduced motion the count does not happen at all.
*/
function Counter({ value, active, delay }: { value: string; active: boolean; delay: number }) {
const target = Number(value.replace(/,/g, ""));
const [current, setCurrent] = useState(0);
useEffect(() => {
if (!active) return;
if (!Number.isFinite(target)) return;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
setCurrent(target);
return;
}
let frame = 0;
const duration = 1450;
const startsAt = performance.now() + delay;
const step = (now: number) => {
if (now < startsAt) {
frame = requestAnimationFrame(step);
return;
}
const progress = Math.min(1, (now - startsAt) / duration);
setCurrent(Math.round(target * (1 - Math.pow(1 - progress, 3))));
if (progress < 1) frame = requestAnimationFrame(step);
};
frame = requestAnimationFrame(step);
return () => cancelAnimationFrame(frame);
}, [active, delay, target]);
if (!Number.isFinite(target)) return <span>{value}</span>;
return (
<span aria-label={value}>
<span aria-hidden="true">{FORMAT.format(current)}</span>
</span>
);
}
export function ImpactStats(props: ImpactStatsProps) {
const p = { ...DEFAULTS, ...props };
const stats = p.stats.length ? p.stats : DEFAULTS.stats;
const sectionRef = useRef<HTMLElement>(null);
const [visible, setVisible] = useState(false);
useEffect(() => {
const section = sectionRef.current;
if (!section) return;
const observer = new IntersectionObserver(
([entry]) => {
if (!entry.isIntersecting) return;
setVisible(true);
observer.disconnect();
},
{ threshold: 0.22 },
);
observer.observe(section);
return () => observer.disconnect();
}, []);
return (
<section
ref={sectionRef}
data-block="ImpactStats"
data-tone={p.tone}
data-visible={visible}
aria-labelledby="impact-stats-title"
>
<style dangerouslySetInnerHTML={{ __html: STYLES }} />
<header>
<h2 id="impact-stats-title" className="is-statement">
<span>{p.lead}</span> {p.statement}
</h2>
</header>
<div className="is-stats">
{stats.map((stat, index) => (
<article key={stat.label} className="is-stat" style={{ ["--is-index" as string]: index }}>
<div className="is-upper">
<p className="is-category">{stat.category}</p>
<p className="is-number">
<Counter value={stat.value} active={visible} delay={160 + index * 90} />
</p>
</div>
<p className="is-label">{stat.label}</p>
</article>
))}
</div>
</section>
);
}
/* A named export as well as the default, so a project that takes this file can
import it either way. */
export default ImpactStats;