25 CSS Number Counter Animations14 / 25

Light JSMIT licensed

Community Growth & User Milestone Counter

Social proof that earns its place: an overlapping avatar stack, a member count that climbs with a per-digit roll, a milestone ribbon that fires at each round thousand, and a 7-day growth bar chart drawn in pure CSS grid. Built for the 'Join 50,000+ developers' block that sits above every newsletter form.

Published Updated

Live Demo
Try it

The code

<section class="cnc-14" aria-label="Community growth and milestone counter demo">
  <div class="cnc-14__stage">
    <article class="cnc-14__card">
      <div class="cnc-14__stack" aria-hidden="true">
        <img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?q=80&w=120&h=120&fit=crop&auto=format" alt="" width="44" height="44">
        <img src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?q=80&w=120&h=120&fit=crop&auto=format" alt="" width="44" height="44">
        <img src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=120&h=120&fit=crop&auto=format" alt="" width="44" height="44">
        <img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=120&h=120&fit=crop&auto=format" alt="" width="44" height="44">
        <img src="https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=120&h=120&fit=crop&auto=format" alt="" width="44" height="44">
        <span class="cnc-14__more">+9k</span>
      </div>
      <p class="cnc-14__count"><span class="cnc-14__num">0</span><span class="cnc-14__plus">+</span></p>
      <p class="cnc-14__label">developers building with Codefronts</p>
      <div class="cnc-14__chart" aria-hidden="true">
        <span style="--cnc-14-v:38"></span><span style="--cnc-14-v:52"></span><span style="--cnc-14-v:44"></span>
        <span style="--cnc-14-v:67"></span><span style="--cnc-14-v:71"></span><span style="--cnc-14-v:86"></span><span style="--cnc-14-v:100"></span>
      </div>
      <p class="cnc-14__foot"><b class="cnc-14__week">+1,842</b> joined in the last 7 days</p>
      <button class="cnc-14__cta" type="button">Join the community</button>
      <p class="cnc-14__ribbon" aria-hidden="true"></p>
      <p class="cnc-14__sr" role="status" aria-live="polite"></p>
    </article>
    <p class="cnc-14__chip" aria-hidden="true">descending z-index stack · floor() milestone crossing · grid bar chart, no library</p>
  </div>
</section>
.cnc-14,
.cnc-14 *,
.cnc-14 *::before,
.cnc-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cnc-14 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --cnc-14-bg: oklch(0.97 0.008 280);
  --cnc-14-card: oklch(1 0 0);
  --cnc-14-ink: oklch(0.2 0.02 280);
  --cnc-14-mut: oklch(0.55 0.02 280);
  --cnc-14-acc: oklch(0.58 0.2 285);
  background: var(--cnc-14-bg);
  color: var(--cnc-14-ink);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
}

.cnc-14__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 18px;
  padding: clamp(18px,4vw,48px);
}

.cnc-14__card {
  position: relative;
  width: min(100%,420px);
  padding: clamp(22px,4vw,30px);
  border-radius: 24px;
  overflow: hidden;
  text-align: center;
  background: var(--cnc-14-card);
  box-shadow: 0 1px 2px oklch(0 0 0/.05),0 26px 60px oklch(0.35 0.06 285/.16);
}

.cnc-14__stack {
  display: flex;
  justify-content: center;
  align-items: center;
}

.cnc-14__stack img {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  object-fit: cover;
  border: 3px solid var(--cnc-14-card);
  margin-left: -12px;
  background: oklch(0.86 0.03 280);
  transition: translate .25s cubic-bezier(.16,1,.3,1);
}

.cnc-14__stack img:first-child {
  margin-left: 0;
}

.cnc-14__stack img:nth-child(1) {
  z-index: 6;
}

.cnc-14__stack img:nth-child(2) {
  z-index: 5;
}

.cnc-14__stack img:nth-child(3) {
  z-index: 4;
}

.cnc-14__stack img:nth-child(4) {
  z-index: 3;
}

.cnc-14__stack img:nth-child(5) {
  z-index: 2;
}

.cnc-14__stack img:hover {
  translate: 0 -5px;
  z-index: 9;
}

.cnc-14__more {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-left: -12px;
  border-radius: 50%;
  border: 3px solid var(--cnc-14-card);
  background: var(--cnc-14-acc);
  color: oklch(0.99 0 0);
  font-size: 11.5px;
  font-weight: 750;
  z-index: 1;
}

.cnc-14__count {
  display: flex;
  align-items: baseline;
  justify-content: center;
  margin-top: 20px;
  font-size: clamp(40px,8vw,58px);
  font-weight: 770;
  letter-spacing: -.05em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

.cnc-14__plus {
  color: var(--cnc-14-acc);
}

.cnc-14__label {
  margin-top: 8px;
  font-size: 14px;
  color: var(--cnc-14-mut);
}

.cnc-14__chart {
  display: grid;
  grid-template-columns: repeat(7,1fr);
  align-items: end;
  gap: 6px;
  height: 64px;
  margin-top: 22px;
}

.cnc-14__chart span {
  height: calc(var(--cnc-14-v) * 1%);
  border-radius: 5px 5px 2px 2px;
  background: color-mix(in oklch,var(--cnc-14-acc) calc(var(--cnc-14-v) * 0.9%),oklch(0.9 0.02 285));
  transform-origin: bottom;
  animation: cnc-14-grow .8s cubic-bezier(.16,1,.3,1) both;
}

.cnc-14__chart span:nth-child(2) {
  animation-delay: .06s;
}

.cnc-14__chart span:nth-child(3) {
  animation-delay: .12s;
}

.cnc-14__chart span:nth-child(4) {
  animation-delay: .18s;
}

.cnc-14__chart span:nth-child(5) {
  animation-delay: .24s;
}

.cnc-14__chart span:nth-child(6) {
  animation-delay: .3s;
}

.cnc-14__chart span:nth-child(7) {
  animation-delay: .36s;
}

@keyframes cnc-14-grow {
  from {
    transform: scaleY(0);
    opacity: .4;
  }

  to {
    transform: scaleY(1);
    opacity: 1;
  }
}

.cnc-14__foot {
  margin-top: 12px;
  font-size: 12.5px;
  color: var(--cnc-14-mut);
}

.cnc-14__foot b {
  color: var(--cnc-14-acc);
  font-weight: 720;
  font-variant-numeric: tabular-nums;
}

.cnc-14__cta {
  width: 100%;
  min-height: 48px;
  margin-top: 20px;
  border: 0;
  border-radius: 14px;
  background: var(--cnc-14-ink);
  color: oklch(0.99 0 0);
  font: inherit;
  font-size: 14.5px;
  font-weight: 660;
  cursor: pointer;
  transition: background .2s,transform .2s;
}

.cnc-14__cta:hover {
  background: var(--cnc-14-acc);
  transform: translateY(-2px);
}

.cnc-14__cta:focus-visible {
  outline: 3px solid var(--cnc-14-acc);
  outline-offset: 3px;
}

.cnc-14__ribbon {
  position: absolute;
  top: 14px;
  right: -42px;
  width: 160px;
  padding: 6px 0;
  rotate: 38deg;
  text-align: center;
  background: var(--cnc-14-acc);
  color: oklch(0.99 0 0);
  font-size: 11.5px;
  font-weight: 750;
  letter-spacing: .04em;
  opacity: 0;
  translate: 0 -10px;
  transition: opacity .45s,translate .45s cubic-bezier(.16,1,.3,1);
}

.cnc-14__card[data-milestone] .cnc-14__ribbon {
  opacity: 1;
  translate: 0 0;
}

.cnc-14__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.cnc-14__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  color: var(--cnc-14-mut);
  padding: 7px 14px;
  border: 1px solid oklch(0 0 0/.12);
  border-radius: 99px;
  text-align: center;
  max-width: min(100%,480px);
}

@media (prefers-reduced-motion: reduce) {
  .cnc-14 * {
    animation: none !important;
    transition-duration: .01ms !important;
  }
}
(() => {
  const nf = new Intl.NumberFormat('en-US');
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  document.querySelectorAll('.cnc-14__card').forEach((card) => {
    if (card.dataset.wired) return;
    card.dataset.wired = '1';
    const numEl = card.querySelector('.cnc-14__num');
    const ribbon = card.querySelector('.cnc-14__ribbon');
    const say = card.querySelector('.cnc-14__sr');
    let members = 50984;

    const roll = (from, to, ms = 1600) => {
      if (reduce) { numEl.textContent = nf.format(to); return; }
      const t0 = performance.now();
      const frame = (t) => {
        const p = Math.min(1, (t - t0) / ms);
        numEl.textContent = nf.format(p === 1 ? to : Math.round(from + (to - from) * (1 - Math.pow(1 - p, 4))));
        if (p < 1) requestAnimationFrame(frame);
      };
      requestAnimationFrame(frame);
    };

    const grow = (by) => {
      const prev = members; members += by;
      if (Math.floor(members / 1000) > Math.floor(prev / 1000)) {   // crossing, not equality
        card.dataset.milestone = '1';
        ribbon.textContent = Math.floor(members / 1000) + 'k members';
        say.textContent = Math.floor(members / 1000) + ' thousand members reached';
        clearTimeout(card._mt);
        card._mt = setTimeout(() => delete card.dataset.milestone, 4200);
      }
      roll(prev, members, 700);
    };

    roll(members - 2400, members);
    const id = setInterval(() => { if (document.visibilityState === 'visible') grow(1 + Math.floor(Math.random() * 4)); }, 3200);
    card.querySelector('.cnc-14__cta').addEventListener('click', () => { grow(1); say.textContent = 'Welcome. You are member ' + nf.format(members); });
    addEventListener('pagehide', () => clearInterval(id), { once: true });
  });
})();
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Number Counter Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Community Growth & User Milestone Counter
Source: https://codefronts.com/motion/css-number-counter-animations/community-milestone/

Social proof that earns its place: an overlapping avatar stack, a member count that climbs with a per-digit roll, a milestone ribbon that fires at each round thousand, and a 7-day growth bar chart drawn in pure CSS grid. Built for the 'Join 50,000+ developers' block that sits above every newsletter form.
## HTML
```html
<section class="cnc-14" aria-label="Community growth and milestone counter demo">
  <div class="cnc-14__stage">
    <article class="cnc-14__card">
      <div class="cnc-14__stack" aria-hidden="true">
        <img src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?q=80&w=120&h=120&fit=crop&auto=format" alt="" width="44" height="44">
        <img src="https://images.unsplash.com/photo-1500648767791-00dcc994a43e?q=80&w=120&h=120&fit=crop&auto=format" alt="" width="44" height="44">
        <img src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?q=80&w=120&h=120&fit=crop&auto=format" alt="" width="44" height="44">
        <img src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?q=80&w=120&h=120&fit=crop&auto=format" alt="" width="44" height="44">
        <img src="https://images.unsplash.com/photo-1544005313-94ddf0286df2?q=80&w=120&h=120&fit=crop&auto=format" alt="" width="44" height="44">
        <span class="cnc-14__more">+9k</span>
      </div>
      <p class="cnc-14__count"><span class="cnc-14__num">0</span><span class="cnc-14__plus">+</span></p>
      <p class="cnc-14__label">developers building with Codefronts</p>
      <div class="cnc-14__chart" aria-hidden="true">
        <span style="--cnc-14-v:38"></span><span style="--cnc-14-v:52"></span><span style="--cnc-14-v:44"></span>
        <span style="--cnc-14-v:67"></span><span style="--cnc-14-v:71"></span><span style="--cnc-14-v:86"></span><span style="--cnc-14-v:100"></span>
      </div>
      <p class="cnc-14__foot"><b class="cnc-14__week">+1,842</b> joined in the last 7 days</p>
      <button class="cnc-14__cta" type="button">Join the community</button>
      <p class="cnc-14__ribbon" aria-hidden="true"></p>
      <p class="cnc-14__sr" role="status" aria-live="polite"></p>
    </article>
    <p class="cnc-14__chip" aria-hidden="true">descending z-index stack · floor() milestone crossing · grid bar chart, no library</p>
  </div>
</section>
```
## CSS
```css
.cnc-14,
.cnc-14 *,
.cnc-14 *::before,
.cnc-14 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cnc-14 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --cnc-14-bg: oklch(0.97 0.008 280);
  --cnc-14-card: oklch(1 0 0);
  --cnc-14-ink: oklch(0.2 0.02 280);
  --cnc-14-mut: oklch(0.55 0.02 280);
  --cnc-14-acc: oklch(0.58 0.2 285);
  background: var(--cnc-14-bg);
  color: var(--cnc-14-ink);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
}

.cnc-14__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  align-content: center;
  gap: 18px;
  padding: clamp(18px,4vw,48px);
}

.cnc-14__card {
  position: relative;
  width: min(100%,420px);
  padding: clamp(22px,4vw,30px);
  border-radius: 24px;
  overflow: hidden;
  text-align: center;
  background: var(--cnc-14-card);
  box-shadow: 0 1px 2px oklch(0 0 0/.05),0 26px 60px oklch(0.35 0.06 285/.16);
}

.cnc-14__stack {
  display: flex;
  justify-content: center;
  align-items: center;
}

.cnc-14__stack img {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  object-fit: cover;
  border: 3px solid var(--cnc-14-card);
  margin-left: -12px;
  background: oklch(0.86 0.03 280);
  transition: translate .25s cubic-bezier(.16,1,.3,1);
}

.cnc-14__stack img:first-child {
  margin-left: 0;
}

.cnc-14__stack img:nth-child(1) {
  z-index: 6;
}

.cnc-14__stack img:nth-child(2) {
  z-index: 5;
}

.cnc-14__stack img:nth-child(3) {
  z-index: 4;
}

.cnc-14__stack img:nth-child(4) {
  z-index: 3;
}

.cnc-14__stack img:nth-child(5) {
  z-index: 2;
}

.cnc-14__stack img:hover {
  translate: 0 -5px;
  z-index: 9;
}

.cnc-14__more {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-left: -12px;
  border-radius: 50%;
  border: 3px solid var(--cnc-14-card);
  background: var(--cnc-14-acc);
  color: oklch(0.99 0 0);
  font-size: 11.5px;
  font-weight: 750;
  z-index: 1;
}

.cnc-14__count {
  display: flex;
  align-items: baseline;
  justify-content: center;
  margin-top: 20px;
  font-size: clamp(40px,8vw,58px);
  font-weight: 770;
  letter-spacing: -.05em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

.cnc-14__plus {
  color: var(--cnc-14-acc);
}

.cnc-14__label {
  margin-top: 8px;
  font-size: 14px;
  color: var(--cnc-14-mut);
}

.cnc-14__chart {
  display: grid;
  grid-template-columns: repeat(7,1fr);
  align-items: end;
  gap: 6px;
  height: 64px;
  margin-top: 22px;
}

.cnc-14__chart span {
  height: calc(var(--cnc-14-v) * 1%);
  border-radius: 5px 5px 2px 2px;
  background: color-mix(in oklch,var(--cnc-14-acc) calc(var(--cnc-14-v) * 0.9%),oklch(0.9 0.02 285));
  transform-origin: bottom;
  animation: cnc-14-grow .8s cubic-bezier(.16,1,.3,1) both;
}

.cnc-14__chart span:nth-child(2) {
  animation-delay: .06s;
}

.cnc-14__chart span:nth-child(3) {
  animation-delay: .12s;
}

.cnc-14__chart span:nth-child(4) {
  animation-delay: .18s;
}

.cnc-14__chart span:nth-child(5) {
  animation-delay: .24s;
}

.cnc-14__chart span:nth-child(6) {
  animation-delay: .3s;
}

.cnc-14__chart span:nth-child(7) {
  animation-delay: .36s;
}

@keyframes cnc-14-grow {
  from {
    transform: scaleY(0);
    opacity: .4;
  }

  to {
    transform: scaleY(1);
    opacity: 1;
  }
}

.cnc-14__foot {
  margin-top: 12px;
  font-size: 12.5px;
  color: var(--cnc-14-mut);
}

.cnc-14__foot b {
  color: var(--cnc-14-acc);
  font-weight: 720;
  font-variant-numeric: tabular-nums;
}

.cnc-14__cta {
  width: 100%;
  min-height: 48px;
  margin-top: 20px;
  border: 0;
  border-radius: 14px;
  background: var(--cnc-14-ink);
  color: oklch(0.99 0 0);
  font: inherit;
  font-size: 14.5px;
  font-weight: 660;
  cursor: pointer;
  transition: background .2s,transform .2s;
}

.cnc-14__cta:hover {
  background: var(--cnc-14-acc);
  transform: translateY(-2px);
}

.cnc-14__cta:focus-visible {
  outline: 3px solid var(--cnc-14-acc);
  outline-offset: 3px;
}

.cnc-14__ribbon {
  position: absolute;
  top: 14px;
  right: -42px;
  width: 160px;
  padding: 6px 0;
  rotate: 38deg;
  text-align: center;
  background: var(--cnc-14-acc);
  color: oklch(0.99 0 0);
  font-size: 11.5px;
  font-weight: 750;
  letter-spacing: .04em;
  opacity: 0;
  translate: 0 -10px;
  transition: opacity .45s,translate .45s cubic-bezier(.16,1,.3,1);
}

.cnc-14__card[data-milestone] .cnc-14__ribbon {
  opacity: 1;
  translate: 0 0;
}

.cnc-14__sr {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.cnc-14__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  color: var(--cnc-14-mut);
  padding: 7px 14px;
  border: 1px solid oklch(0 0 0/.12);
  border-radius: 99px;
  text-align: center;
  max-width: min(100%,480px);
}

@media (prefers-reduced-motion: reduce) {
  .cnc-14 * {
    animation: none !important;
    transition-duration: .01ms !important;
  }
}
```

## JavaScript
```js
(() => {
  const nf = new Intl.NumberFormat('en-US');
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  document.querySelectorAll('.cnc-14__card').forEach((card) => {
    if (card.dataset.wired) return;
    card.dataset.wired = '1';
    const numEl = card.querySelector('.cnc-14__num');
    const ribbon = card.querySelector('.cnc-14__ribbon');
    const say = card.querySelector('.cnc-14__sr');
    let members = 50984;

    const roll = (from, to, ms = 1600) => {
      if (reduce) { numEl.textContent = nf.format(to); return; }
      const t0 = performance.now();
      const frame = (t) => {
        const p = Math.min(1, (t - t0) / ms);
        numEl.textContent = nf.format(p === 1 ? to : Math.round(from + (to - from) * (1 - Math.pow(1 - p, 4))));
        if (p < 1) requestAnimationFrame(frame);
      };
      requestAnimationFrame(frame);
    };

    const grow = (by) => {
      const prev = members; members += by;
      if (Math.floor(members / 1000) > Math.floor(prev / 1000)) {   // crossing, not equality
        card.dataset.milestone = '1';
        ribbon.textContent = Math.floor(members / 1000) + 'k members';
        say.textContent = Math.floor(members / 1000) + ' thousand members reached';
        clearTimeout(card._mt);
        card._mt = setTimeout(() => delete card.dataset.milestone, 4200);
      }
      roll(prev, members, 700);
    };

    roll(members - 2400, members);
    const id = setInterval(() => { if (document.visibilityState === 'visible') grow(1 + Math.floor(Math.random() * 4)); }, 3200);
    card.querySelector('.cnc-14__cta').addEventListener('click', () => { grow(1); say.textContent = 'Welcome. You are member ' + nf.format(members); });
    addEventListener('pagehide', () => clearInterval(id), { once: true });
  });
})();
```

How this works

The avatar stack is a flex row with negative margins and a reversed stacking order, so each face sits under the one to its left — the detail that makes the group read as depth rather than a row:

.stack { display: flex; }
.stack img { margin-left: -12px; }
.stack img:nth-child(1) { z-index: 5 }  /* descending */
.stack img:hover { translate: 0 -4px; z-index: 9 }

The milestone is a comparison against the previous value, not the current one:

const crossed = Math.floor(next / 1000) > Math.floor(prev / 1000);
if (crossed) root.dataset.milestone = Math.floor(next / 1000) + 'k';

That single line is why the ribbon fires exactly once at 51,000 instead of continuously above it. The bar chart is a CSS grid where each column's height is calc(var(--v) * 1%) — seven inline custom properties, no chart runtime, and it reflows perfectly at any width because the grid columns are 1fr.

Make it yours

  • Real avatars: swap the image sources for your community's profile pictures; keep width/height attributes to prevent layout shift.
  • Live socket count: the render function is idempotent — call it from a socket message and the roll animation handles the rest.
  • Different milestone cadence: change the divisor (1000 → 100 or 10000) to match your growth rate; the crossing logic is unchanged.
  • Regional split: add a second row of flag chips with percentages under the count for community-of-communities products.

Gotchas — read before shipping

  • Overlapping avatars without descending z-index look like a broken row on Safari — set it explicitly rather than relying on source order.
  • Always give <img> explicit width and height (or aspect-ratio) — an avatar stack that loads late shifts the entire block and hurts CLS.
  • Milestone logic based on 'value % 1000 === 0' misses jumps of more than one at a time. Compare floor(prev) with floor(next).
  • Fake growth counters are a credibility risk — if the number cannot be verified, show a real, static one instead.
  • Decorative avatars need alt="" — describing five random faces adds noise for screen reader users.

Browser support

ChromeSafariFirefoxEdge
111+16.4+113+111+

Flex, grid and negative margins are universal. Only the oklch/color-mix theming is modern; the layout is unaffected in older engines.

Search CodeFronts

Loading…