Open demo

CSS code

HTML
<div><span class="lbl">Count on load</span>
<ul class="mc mc-tiles" aria-label="Wordhaven this term">
  <li><p class="mc-num" style="--to:742"><span class="mc-val">742</span></p><p>schools teach with Wordhaven</p></li>
  <li><p class="mc-num" style="--to:98" data-suffix="%"><span class="mc-val">98%</span></p><p>of teachers renew each year</p></li>
  <li><p class="mc-num is-tenths" style="--to:42" data-suffix="M"><span class="mc-val">4.2M</span></p><p>lessons finished this term</p></li>
</ul></div>

<div><span class="lbl">Count on scroll</span>
<section class="mc mc-scroll" aria-labelledby="mc-scroll-title">
  <h3 id="mc-scroll-title">Kiln is built in the open</h3>
  <p>The numbers run as each one scrolls into view, driven by the scroll position rather than a timer.</p>
  <ul>
    <li><p class="mc-num" style="--to:312"><span class="mc-val">312</span></p><p>people have contributed code</p></li>
    <li><p class="mc-num" style="--to:87" data-suffix="%"><span class="mc-val">87%</span></p><p>test coverage on the main branch</p></li>
    <li><p class="mc-num is-tenths" style="--to:126" data-suffix="k"><span class="mc-val">12.6k</span></p><p>stars from developers on GitHub</p></li>
  </ul>
</section></div>

<div><span class="lbl">Container queries: sidebar and main column</span>
<div class="mc mc-demo">
  <div class="mc-cq"><section class="mc-cq-box" aria-labelledby="mc-cq-1"><h3 id="mc-cq-1">Wordhaven this term</h3><ul>
    <li><p class="mc-num" style="--to:742"><span class="mc-val">742</span></p><p>schools</p></li>
    <li><p class="mc-num" style="--to:98" data-suffix="%"><span class="mc-val">98%</span></p><p>teachers renew</p></li>
    <li><p class="mc-num is-tenths" style="--to:42" data-suffix="M"><span class="mc-val">4.2M</span></p><p>lessons finished</p></li>
    <li><p class="mc-num" style="--to:36"><span class="mc-val">36</span></p><p>languages</p></li>
  </ul></section></div>
  <div class="mc-cq"><section class="mc-cq-box" aria-labelledby="mc-cq-2"><h3 id="mc-cq-2">Wordhaven this term</h3><ul>
    <li><p class="mc-num" style="--to:742"><span class="mc-val">742</span></p><p>schools</p></li>
    <li><p class="mc-num" style="--to:98" data-suffix="%"><span class="mc-val">98%</span></p><p>teachers renew</p></li>
    <li><p class="mc-num is-tenths" style="--to:42" data-suffix="M"><span class="mc-val">4.2M</span></p><p>lessons finished</p></li>
    <li><p class="mc-num" style="--to:36"><span class="mc-val">36</span></p><p>languages</p></li>
  </ul></section></div>
</div></div>
CSS
/* CSS-only animated counters, no JavaScript.
   Each .mc-num holds the real number as text (.mc-val) and its target as --to. Where the browser can animate a
   registered integer and print it with counter(), the text is visually hidden (screen readers still read it) and
   ::after shows the counting copy, with empty alt text so it is not read twice. Everywhere else, and under
   prefers-reduced-motion, the plain text simply shows. counter() cannot print thousands separators, so the
   figures use compact units (k, M); .is-tenths prints one decimal place from --to (42 → 4.2). */
@property --mc-n{syntax:"<integer>";initial-value:0;inherits:false}
@keyframes mc-count{from{--mc-n:0}}
.mc{--mc-ink:#16161d;--mc-muted:#55555f;--mc-line:#e3e3dc;--mc-accent:#3b2fc9;--mc-hi:#fde68a;color:var(--mc-ink)}
.mc-num{font-weight:800;letter-spacing:-.035em;line-height:1;font-variant-numeric:tabular-nums;white-space:nowrap}
/* WebKit does not repaint counter() while a registered property animates, so Safari keeps the plain number */
@supports (counter-reset:mc mod(7,3)) and (not (hanging-punctuation:first)){
  @media (prefers-reduced-motion:no-preference){
    .mc-num{--mc-n:var(--to);counter-reset:mc var(--mc-n) mc-a round(down,var(--mc-n) / 10,1) mc-b mod(var(--mc-n),10);
      animation:mc-count 2.2s cubic-bezier(.16,1,.3,1) both}
    .mc-num .mc-val{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
    .mc-num::after{content:attr(data-prefix) counter(mc) attr(data-suffix);content:attr(data-prefix) counter(mc) attr(data-suffix) / ""}
    .mc-num.is-tenths::after{content:attr(data-prefix) counter(mc-a) "." counter(mc-b) attr(data-suffix);
      content:attr(data-prefix) counter(mc-a) "." counter(mc-b) attr(data-suffix) / ""}
    /* scroll-driven: the count runs while the number scrolls into view, and is complete once it is fully visible */
    @supports (animation-timeline:view()){
      .mc-scroll .mc-num{animation-timeline:view();animation-range:entry 0% entry 100%}
    }
  }
}

/* 1. Count on load */
.mc-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,220px),1fr));gap:16px;list-style:none}
.mc-tiles li{background:#fff;border:1px solid var(--mc-line);border-radius:18px;padding:22px 22px 20px;box-shadow:0 10px 26px -18px rgba(22,22,29,.3)}
.mc-tiles .mc-num{font-size:clamp(2.8rem,6vw,3.6rem);color:var(--mc-accent)}
.mc-tiles p:last-child{margin-top:10px;color:var(--mc-muted);font-size:.92rem}

/* 2. Count on scroll */
.mc-scroll{background:var(--mc-ink);color:#f4f4f6;border-radius:24px;padding:clamp(24px,4vw,44px)}
.mc-scroll h3{font-size:clamp(1.3rem,3vw,1.7rem);letter-spacing:-.02em}
.mc-scroll > p{margin-top:6px;color:#b9b9c6;max-width:56ch}
.mc-scroll ul{list-style:none;display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:20px;margin-top:28px}
.mc-scroll li{border-left:3px solid var(--mc-hi);padding-left:16px}
.mc-scroll .mc-num{font-size:clamp(2.4rem,5vw,3.2rem);color:#fff}
.mc-scroll li p:last-child{margin-top:6px;color:#b9b9c6;font-size:.9rem}

/* 3. Container queries: the same component reflows to the width of whatever column it lands in */
.mc-demo{display:grid;grid-template-columns:minmax(0,280px) minmax(0,1fr);gap:20px;align-items:start}
.mc-cq{container-type:inline-size}
.mc-cq-box{background:#fff;border:1px solid var(--mc-line);border-radius:18px;padding:18px 20px}
.mc-cq-box h3{font-size:.8rem;letter-spacing:.1em;text-transform:uppercase;color:var(--mc-muted)}
.mc-cq-box ul{list-style:none;display:grid;margin-top:8px}
.mc-cq-box li{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:10px 0;border-top:1px solid var(--mc-line)}
.mc-cq-box li:first-child{border-top:0}
.mc-cq-box .mc-num{font-size:1.5rem;color:var(--mc-accent);order:2}
.mc-cq-box li p:last-child{font-size:.88rem;color:var(--mc-muted)}
@container (min-width:30rem){
  .mc-cq-box{padding:24px 26px}
  .mc-cq-box ul{grid-template-columns:repeat(2,minmax(0,1fr));gap:18px 28px;margin-top:14px}
  .mc-cq-box li{flex-direction:column;align-items:flex-start;gap:6px;padding:0 0 0 14px;border-top:0;border-left:3px solid var(--mc-hi)}
  .mc-cq-box .mc-num{order:0;font-size:2.3rem}
}
@container (min-width:44rem){.mc-cq-box ul{grid-template-columns:repeat(4,minmax(0,1fr))}.mc-cq-box .mc-num{font-size:2.8rem}}

@media (max-width:760px){.mc-scroll ul{grid-template-columns:1fr}.mc-demo{grid-template-columns:1fr}}

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Count on load</span>
<ul class="mc mc-tiles list-unstyled row row-cols-1 row-cols-sm-2 row-cols-md-3 g-3 mb-0" aria-label="Wordhaven this term">
  <li class="col"><div class="card h-100"><div class="card-body p-4"><p class="mc-num" style="--to:742"><span class="mc-val">742</span></p><p class="text-body-secondary mt-2" style="font-size:.92rem">schools teach with Wordhaven</p></div></div></li>
  <li class="col"><div class="card h-100"><div class="card-body p-4"><p class="mc-num" style="--to:98" data-suffix="%"><span class="mc-val">98%</span></p><p class="text-body-secondary mt-2" style="font-size:.92rem">of teachers renew each year</p></div></div></li>
  <li class="col"><div class="card h-100"><div class="card-body p-4"><p class="mc-num is-tenths" style="--to:42" data-suffix="M"><span class="mc-val">4.2M</span></p><p class="text-body-secondary mt-2" style="font-size:.92rem">lessons finished this term</p></div></div></li>
</ul></div>

<div><span class="lbl">Count on scroll</span>
<section class="mc mc-scroll bg-dark text-white" aria-labelledby="mc-scroll-title" style="--bs-dark-rgb:22,22,29">
  <h3 class="fw-bold mb-0" id="mc-scroll-title" style="font-size:clamp(1.3rem,3vw,1.7rem);letter-spacing:-.02em">Kiln is built in the open</h3>
  <p class="text-muted-dark mt-2" style="max-width:56ch">The numbers run as each one scrolls into view, driven by the scroll position rather than a timer.</p>
  <ul class="list-unstyled row row-cols-1 row-cols-md-3 g-4 mt-1 mb-0">
    <li class="col"><div><p class="mc-num" style="--to:312"><span class="mc-val">312</span></p><p class="text-muted-dark small mt-2">people have contributed code</p></div></li>
    <li class="col"><div><p class="mc-num" style="--to:87" data-suffix="%"><span class="mc-val">87%</span></p><p class="text-muted-dark small mt-2">test coverage on the main branch</p></div></li>
    <li class="col"><div><p class="mc-num is-tenths" style="--to:126" data-suffix="k"><span class="mc-val">12.6k</span></p><p class="text-muted-dark small mt-2">stars from developers on GitHub</p></div></li>
  </ul>
</section></div>

<div><span class="lbl">Container queries: sidebar and main column</span>
<div class="mc row g-3 align-items-start">
  <div class="col-12 col-md-4 col-lg-3"><div class="mc-cq"><section class="card" aria-labelledby="mc-cq-1"><div class="card-body"><h3 class="fw-bold mb-0" id="mc-cq-1">Wordhaven this term</h3><ul class="list-unstyled mt-2 mb-0">
    <li><p class="mc-num" style="--to:742"><span class="mc-val">742</span></p><p class="small text-body-secondary">schools</p></li>
    <li><p class="mc-num" style="--to:98" data-suffix="%"><span class="mc-val">98%</span></p><p class="small text-body-secondary">teachers renew</p></li>
    <li><p class="mc-num is-tenths" style="--to:42" data-suffix="M"><span class="mc-val">4.2M</span></p><p class="small text-body-secondary">lessons finished</p></li>
    <li><p class="mc-num" style="--to:36"><span class="mc-val">36</span></p><p class="small text-body-secondary">languages</p></li>
  </ul></div></section></div></div>
  <div class="col-md"><div class="mc-cq"><section class="card" aria-labelledby="mc-cq-2"><div class="card-body"><h3 class="fw-bold mb-0" id="mc-cq-2">Wordhaven this term</h3><ul class="list-unstyled mt-2 mb-0">
    <li><p class="mc-num" style="--to:742"><span class="mc-val">742</span></p><p class="small text-body-secondary">schools</p></li>
    <li><p class="mc-num" style="--to:98" data-suffix="%"><span class="mc-val">98%</span></p><p class="small text-body-secondary">teachers renew</p></li>
    <li><p class="mc-num is-tenths" style="--to:42" data-suffix="M"><span class="mc-val">4.2M</span></p><p class="small text-body-secondary">lessons finished</p></li>
    <li><p class="mc-num" style="--to:36"><span class="mc-val">36</span></p><p class="small text-body-secondary">languages</p></li>
  </ul></div></section></div></div>
</div></div>
CSS
/* CSS-only animated counters on Bootstrap, no JavaScript. Layout is Bootstrap's grid, .card and utilities;
   the counting is the same small block of CSS as the plain version: a registered integer (--mc-n) animated from 0
   to --to and printed with counter() in ::after, while the real number (.mc-val) stays in the HTML for screen
   readers, Safari (which does not repaint counter() mid-animation) and prefers-reduced-motion. */
@property --mc-n{syntax:"<integer>";initial-value:0;inherits:false}
@keyframes mc-count{from{--mc-n:0}}
.mc{--mc-accent:#3b2fc9;--mc-hi:#fde68a;--mc-line:#e3e3dc;--bs-secondary-color:#55555f;--bs-border-color:#e3e3dc;color:#16161d}
.mc p{margin:0}
.mc-num{font-weight:800;letter-spacing:-.035em;line-height:1;font-variant-numeric:tabular-nums;white-space:nowrap}
@supports (counter-reset:mc mod(7,3)) and (not (hanging-punctuation:first)){
  @media (prefers-reduced-motion:no-preference){
    .mc-num{--mc-n:var(--to);counter-reset:mc var(--mc-n) mc-a round(down,var(--mc-n) / 10,1) mc-b mod(var(--mc-n),10);
      animation:mc-count 2.2s cubic-bezier(.16,1,.3,1) both}
    .mc-num .mc-val{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
    .mc-num::after{content:attr(data-prefix) counter(mc) attr(data-suffix);content:attr(data-prefix) counter(mc) attr(data-suffix) / ""}
    .mc-num.is-tenths::after{content:attr(data-prefix) counter(mc-a) "." counter(mc-b) attr(data-suffix);
      content:attr(data-prefix) counter(mc-a) "." counter(mc-b) attr(data-suffix) / ""}
    @supports (animation-timeline:view()){
      .mc-scroll .mc-num{animation-timeline:view();animation-range:entry 0% entry 100%}
    }
  }
}
.mc .card{--bs-card-border-color:var(--mc-line);--bs-card-border-radius:18px;box-shadow:0 10px 26px -18px rgba(22,22,29,.3)}
.mc-tiles .mc-num{font-size:clamp(2.8rem,6vw,3.6rem);color:var(--mc-accent)}
.mc-scroll{border-radius:24px;padding:clamp(24px,4vw,44px)}
.mc-scroll .mc-num{font-size:clamp(2.4rem,5vw,3.2rem)}
.mc-scroll .col > div{border-left:3px solid var(--mc-hi);padding-left:16px}
.mc-scroll .text-muted-dark{color:#b9b9c6}
.mc-cq{container-type:inline-size}
.mc-cq .card-body{padding:18px 20px}
.mc-cq h3{font-size:.8rem;letter-spacing:.1em;text-transform:uppercase;color:#55555f}
.mc-cq ul{display:grid}
.mc-cq li{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:10px 0;border-top:1px solid var(--mc-line)}
.mc-cq li:first-child{border-top:0}
.mc-cq .mc-num{font-size:1.5rem;color:var(--mc-accent);order:2}
@container (min-width:30rem){
  .mc-cq .card-body{padding:24px 26px}
  .mc-cq ul{grid-template-columns:repeat(2,minmax(0,1fr));gap:18px 28px;margin-top:14px!important}
  .mc-cq li{flex-direction:column;align-items:flex-start;gap:6px;padding:0 0 0 14px;border-top:0;border-left:3px solid var(--mc-hi)}
  .mc-cq .mc-num{order:0;font-size:2.3rem}
}
@container (min-width:44rem){.mc-cq ul{grid-template-columns:repeat(4,minmax(0,1fr))}.mc-cq .mc-num{font-size:2.8rem}}

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#56565f] uppercase">Count on load</span>
<ul class="grid grid-cols-[repeat(auto-fit,minmax(min(100%,220px),1fr))] gap-4" aria-label="Wordhaven this term">
  <li class="rounded-[18px] border border-mc-line bg-white px-[22px] pt-[22px] pb-5 shadow-[0_10px_26px_-18px_rgba(22,22,29,.3)]"><p class="mc-num text-[clamp(2.8rem,6vw,3.6rem)] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-mc-accent tabular-nums" style="--to:742"><span class="mc-val">742</span></p><p class="mt-2.5 text-[.92rem] text-mc-muted">schools teach with Wordhaven</p></li>
  <li class="rounded-[18px] border border-mc-line bg-white px-[22px] pt-[22px] pb-5 shadow-[0_10px_26px_-18px_rgba(22,22,29,.3)]"><p class="mc-num text-[clamp(2.8rem,6vw,3.6rem)] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-mc-accent tabular-nums" style="--to:98" data-suffix="%"><span class="mc-val">98%</span></p><p class="mt-2.5 text-[.92rem] text-mc-muted">of teachers renew each year</p></li>
  <li class="rounded-[18px] border border-mc-line bg-white px-[22px] pt-[22px] pb-5 shadow-[0_10px_26px_-18px_rgba(22,22,29,.3)]"><p class="mc-num is-tenths text-[clamp(2.8rem,6vw,3.6rem)] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-mc-accent tabular-nums" style="--to:42" data-suffix="M"><span class="mc-val">4.2M</span></p><p class="mt-2.5 text-[.92rem] text-mc-muted">lessons finished this term</p></li>
</ul></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#56565f] uppercase">Count on scroll</span>
<section class="mc-scroll rounded-3xl bg-mc-ink p-[clamp(24px,4vw,44px)] text-[#f4f4f6]" aria-labelledby="mc-scroll-title">
  <h3 class="text-[clamp(1.3rem,3vw,1.7rem)] font-bold tracking-[-.02em]" id="mc-scroll-title">Kiln is built in the open</h3>
  <p class="mt-1.5 max-w-[56ch] text-[#b9b9c6]">The numbers run as each one scrolls into view, driven by the scroll position rather than a timer.</p>
  <ul class="mt-7 grid grid-cols-1 gap-5 min-[761px]:grid-cols-3">
    <li class="border-l-[3px] border-mc-hi pl-4"><p class="mc-num text-[clamp(2.4rem,5vw,3.2rem)] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-white tabular-nums" style="--to:312"><span class="mc-val">312</span></p><p class="mt-1.5 text-[.9rem] text-[#b9b9c6]">people have contributed code</p></li>
    <li class="border-l-[3px] border-mc-hi pl-4"><p class="mc-num text-[clamp(2.4rem,5vw,3.2rem)] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-white tabular-nums" style="--to:87" data-suffix="%"><span class="mc-val">87%</span></p><p class="mt-1.5 text-[.9rem] text-[#b9b9c6]">test coverage on the main branch</p></li>
    <li class="border-l-[3px] border-mc-hi pl-4"><p class="mc-num is-tenths text-[clamp(2.4rem,5vw,3.2rem)] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-white tabular-nums" style="--to:126" data-suffix="k"><span class="mc-val">12.6k</span></p><p class="mt-1.5 text-[.9rem] text-[#b9b9c6]">stars from developers on GitHub</p></li>
  </ul>
</section></div>

<div><span class="mb-4 block text-[.72rem] tracking-[.12em] text-[#56565f] uppercase">Container queries: sidebar and main column</span>
<div class="grid grid-cols-1 items-start gap-5 min-[761px]:grid-cols-[minmax(0,280px)_minmax(0,1fr)]">
  <div class="@container"><section class="rounded-[18px] border border-mc-line bg-white px-5 py-[18px] @min-[30rem]:px-[26px] @min-[30rem]:py-6" aria-labelledby="mc-cq-1"><h3 class="text-[.8rem] font-bold tracking-[.1em] text-mc-muted uppercase" id="mc-cq-1">Wordhaven this term</h3>
    <ul class="mt-2 grid @min-[30rem]:mt-3.5 @min-[30rem]:grid-cols-2 @min-[30rem]:gap-x-7 @min-[30rem]:gap-y-[18px] @min-[44rem]:grid-cols-4">
      <li class="flex items-baseline justify-between gap-3 border-t border-mc-line py-2.5 first:border-t-0 @min-[30rem]:flex-col @min-[30rem]:items-start @min-[30rem]:gap-1.5 @min-[30rem]:border-t-0 @min-[30rem]:border-l-[3px] @min-[30rem]:border-mc-hi @min-[30rem]:py-0 @min-[30rem]:pl-3.5"><p class="mc-num order-2 text-[1.5rem] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-mc-accent tabular-nums @min-[30rem]:order-none @min-[30rem]:text-[2.3rem] @min-[44rem]:text-[2.8rem]" style="--to:742"><span class="mc-val">742</span></p><p class="text-[.88rem] text-mc-muted">schools</p></li>
      <li class="flex items-baseline justify-between gap-3 border-t border-mc-line py-2.5 first:border-t-0 @min-[30rem]:flex-col @min-[30rem]:items-start @min-[30rem]:gap-1.5 @min-[30rem]:border-t-0 @min-[30rem]:border-l-[3px] @min-[30rem]:border-mc-hi @min-[30rem]:py-0 @min-[30rem]:pl-3.5"><p class="mc-num order-2 text-[1.5rem] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-mc-accent tabular-nums @min-[30rem]:order-none @min-[30rem]:text-[2.3rem] @min-[44rem]:text-[2.8rem]" style="--to:98" data-suffix="%"><span class="mc-val">98%</span></p><p class="text-[.88rem] text-mc-muted">teachers renew</p></li>
      <li class="flex items-baseline justify-between gap-3 border-t border-mc-line py-2.5 first:border-t-0 @min-[30rem]:flex-col @min-[30rem]:items-start @min-[30rem]:gap-1.5 @min-[30rem]:border-t-0 @min-[30rem]:border-l-[3px] @min-[30rem]:border-mc-hi @min-[30rem]:py-0 @min-[30rem]:pl-3.5"><p class="mc-num is-tenths order-2 text-[1.5rem] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-mc-accent tabular-nums @min-[30rem]:order-none @min-[30rem]:text-[2.3rem] @min-[44rem]:text-[2.8rem]" style="--to:42" data-suffix="M"><span class="mc-val">4.2M</span></p><p class="text-[.88rem] text-mc-muted">lessons finished</p></li>
      <li class="flex items-baseline justify-between gap-3 border-t border-mc-line py-2.5 first:border-t-0 @min-[30rem]:flex-col @min-[30rem]:items-start @min-[30rem]:gap-1.5 @min-[30rem]:border-t-0 @min-[30rem]:border-l-[3px] @min-[30rem]:border-mc-hi @min-[30rem]:py-0 @min-[30rem]:pl-3.5"><p class="mc-num order-2 text-[1.5rem] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-mc-accent tabular-nums @min-[30rem]:order-none @min-[30rem]:text-[2.3rem] @min-[44rem]:text-[2.8rem]" style="--to:36"><span class="mc-val">36</span></p><p class="text-[.88rem] text-mc-muted">languages</p></li>
    </ul></section></div>
  <div class="@container"><section class="rounded-[18px] border border-mc-line bg-white px-5 py-[18px] @min-[30rem]:px-[26px] @min-[30rem]:py-6" aria-labelledby="mc-cq-2"><h3 class="text-[.8rem] font-bold tracking-[.1em] text-mc-muted uppercase" id="mc-cq-2">Wordhaven this term</h3>
    <ul class="mt-2 grid @min-[30rem]:mt-3.5 @min-[30rem]:grid-cols-2 @min-[30rem]:gap-x-7 @min-[30rem]:gap-y-[18px] @min-[44rem]:grid-cols-4">
      <li class="flex items-baseline justify-between gap-3 border-t border-mc-line py-2.5 first:border-t-0 @min-[30rem]:flex-col @min-[30rem]:items-start @min-[30rem]:gap-1.5 @min-[30rem]:border-t-0 @min-[30rem]:border-l-[3px] @min-[30rem]:border-mc-hi @min-[30rem]:py-0 @min-[30rem]:pl-3.5"><p class="mc-num order-2 text-[1.5rem] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-mc-accent tabular-nums @min-[30rem]:order-none @min-[30rem]:text-[2.3rem] @min-[44rem]:text-[2.8rem]" style="--to:742"><span class="mc-val">742</span></p><p class="text-[.88rem] text-mc-muted">schools</p></li>
      <li class="flex items-baseline justify-between gap-3 border-t border-mc-line py-2.5 first:border-t-0 @min-[30rem]:flex-col @min-[30rem]:items-start @min-[30rem]:gap-1.5 @min-[30rem]:border-t-0 @min-[30rem]:border-l-[3px] @min-[30rem]:border-mc-hi @min-[30rem]:py-0 @min-[30rem]:pl-3.5"><p class="mc-num order-2 text-[1.5rem] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-mc-accent tabular-nums @min-[30rem]:order-none @min-[30rem]:text-[2.3rem] @min-[44rem]:text-[2.8rem]" style="--to:98" data-suffix="%"><span class="mc-val">98%</span></p><p class="text-[.88rem] text-mc-muted">teachers renew</p></li>
      <li class="flex items-baseline justify-between gap-3 border-t border-mc-line py-2.5 first:border-t-0 @min-[30rem]:flex-col @min-[30rem]:items-start @min-[30rem]:gap-1.5 @min-[30rem]:border-t-0 @min-[30rem]:border-l-[3px] @min-[30rem]:border-mc-hi @min-[30rem]:py-0 @min-[30rem]:pl-3.5"><p class="mc-num is-tenths order-2 text-[1.5rem] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-mc-accent tabular-nums @min-[30rem]:order-none @min-[30rem]:text-[2.3rem] @min-[44rem]:text-[2.8rem]" style="--to:42" data-suffix="M"><span class="mc-val">4.2M</span></p><p class="text-[.88rem] text-mc-muted">lessons finished</p></li>
      <li class="flex items-baseline justify-between gap-3 border-t border-mc-line py-2.5 first:border-t-0 @min-[30rem]:flex-col @min-[30rem]:items-start @min-[30rem]:gap-1.5 @min-[30rem]:border-t-0 @min-[30rem]:border-l-[3px] @min-[30rem]:border-mc-hi @min-[30rem]:py-0 @min-[30rem]:pl-3.5"><p class="mc-num order-2 text-[1.5rem] leading-none font-extrabold tracking-[-.035em] whitespace-nowrap text-mc-accent tabular-nums @min-[30rem]:order-none @min-[30rem]:text-[2.3rem] @min-[44rem]:text-[2.8rem]" style="--to:36"><span class="mc-val">36</span></p><p class="text-[.88rem] text-mc-muted">languages</p></li>
    </ul></section></div>
</div></div>
CSS
/* CSS-only animated counters, no JavaScript. Layout and container queries (@container, @min-[30rem]:) are
   utilities in the markup. The counting needs a registered property, keyframes and counter(), so it lives here:
   --mc-n animates from 0 to --to and ::after prints it, while the real number (.mc-val) stays in the HTML for
   screen readers, Safari (which does not repaint counter() mid-animation) and prefers-reduced-motion. */
@theme { --color-mc-ink: #16161d; --color-mc-muted: #55555f; --color-mc-line: #e3e3dc; --color-mc-accent: #3b2fc9; --color-mc-hi: #fde68a; }
@property --mc-n { syntax: "<integer>"; initial-value: 0; inherits: false; }
@keyframes mc-count { from { --mc-n: 0; } }
@supports (counter-reset: mc mod(7, 3)) and (not (hanging-punctuation: first)) {
  @media (prefers-reduced-motion: no-preference) {
    .mc-num { --mc-n: var(--to); counter-reset: mc var(--mc-n) mc-a round(down, var(--mc-n) / 10, 1) mc-b mod(var(--mc-n), 10);
      animation: mc-count 2.2s cubic-bezier(.16, 1, .3, 1) both; }
    .mc-num .mc-val { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
    .mc-num::after { content: attr(data-prefix) counter(mc) attr(data-suffix); content: attr(data-prefix) counter(mc) attr(data-suffix) / ""; }
    .mc-num.is-tenths::after { content: attr(data-prefix) counter(mc-a) "." counter(mc-b) attr(data-suffix);
      content: attr(data-prefix) counter(mc-a) "." counter(mc-b) attr(data-suffix) / ""; }
    @supports (animation-timeline: view()) {
      .mc-scroll .mc-num { animation-timeline: view(); animation-range: entry 0% entry 100%; }
    }
  }
}

About this set

Number counters that animate with no JavaScript. Count on load runs three figures from 0 when the page opens. Count on scroll ties the count to the scroll position with animation-timeline: view(), so each number finishes as it becomes fully visible. Container queries shows one stats component in a narrow sidebar, where it is a list with numbers on the right, and in a wide column, where it becomes a four-up grid. The technique is a registered integer property, @property –mc-n, animated from 0 to the value in –to, and printed in ::after with counter(). A decimal such as 4.2M uses round() and mod() to split the integer into two counters. counter() cannot print thousands separators, so the figures use compact units such as k and M. The real number stays as text in the HTML; where the count can run, it is visually hidden but still read. Safari does not repaint counter() during the animation, so the set keeps the plain number there.

What’s included

  • No JavaScript: @property, @keyframes and counter()
  • Decimal values with round() and mod()
  • Scroll-driven count with animation-timeline: view() and a load-time fallback
  • Container queries reflow the same component from sidebar to main column
  • Plain number shown under reduced motion and where support is missing

Accessibility

The real number is text in the HTML. When the count runs, that text is visually hidden but still read by screen readers, and the ::after copy has empty alt text (content: ... / "") so it is not read twice. Under prefers-reduced-motion, in browsers without the needed features, and in Safari, the plain number simply shows, so nobody sees a count from 0 they did not ask for.

Customise it

Set --to on each .mc-num and data-suffix or data-prefix for units; add .is-tenths for one decimal place. Colours are --mc-accent and --mc-hi on .mc, and the container breakpoints are the @container rules at 30rem and 44rem (in Tailwind, the @min-[30rem]: and @min-[44rem]: variants).