25 CSS Number Counter Animations24 / 25

Light JSMIT licensed

B2B SaaS Analytics Summary Grid

The summary strip at the top of every B2B analytics product: six metric tiles that re-count whenever the date range changes, each with a delta chip and a sparkline that redraws. One dataset, one render function, one cached formatter per metric — the architecture that stops a dashboard turning into six copies of the same counter code.

Published Updated

Live Demo
Try it

The code

<section class="cnc-24" aria-label="B2B SaaS analytics summary grid demo">
  <div class="cnc-24__stage">
    <div class="cnc-24__panel">
      <header class="cnc-24__head">
        <div>
          <h3 class="cnc-24__title">Workspace analytics</h3>
          <p class="cnc-24__sub" id="cnc-24-base">Compared with the previous period · sample data</p>
        </div>
        <div class="cnc-24__seg" role="group" aria-label="Date range">
          <button class="cnc-24__segb" type="button" data-range="7" aria-pressed="false">7d</button>
          <button class="cnc-24__segb" type="button" data-range="30" aria-pressed="true">30d</button>
          <button class="cnc-24__segb" type="button" data-range="90" aria-pressed="false">90d</button>
        </div>
      </header>
      <div class="cnc-24__grid">
        <article class="cnc-24__tile" data-key="mrr"><p class="cnc-24__k">Net MRR</p><p class="cnc-24__v" aria-hidden="true">—</p><p class="cnc-24__d"><span></span></p><svg class="cnc-24__spark" viewBox="0 0 120 34" preserveAspectRatio="none" aria-hidden="true"><polyline points=""/></svg></article>
        <article class="cnc-24__tile" data-key="workspaces"><p class="cnc-24__k">Active workspaces</p><p class="cnc-24__v" aria-hidden="true">—</p><p class="cnc-24__d"><span></span></p><svg class="cnc-24__spark" viewBox="0 0 120 34" preserveAspectRatio="none" aria-hidden="true"><polyline points=""/></svg></article>
        <article class="cnc-24__tile" data-key="seats"><p class="cnc-24__k">Paid seats</p><p class="cnc-24__v" aria-hidden="true">—</p><p class="cnc-24__d"><span></span></p><svg class="cnc-24__spark" viewBox="0 0 120 34" preserveAspectRatio="none" aria-hidden="true"><polyline points=""/></svg></article>
        <article class="cnc-24__tile" data-key="nrr"><p class="cnc-24__k">Net revenue retention</p><p class="cnc-24__v" aria-hidden="true">—</p><p class="cnc-24__d"><span></span></p><svg class="cnc-24__spark" viewBox="0 0 120 34" preserveAspectRatio="none" aria-hidden="true"><polyline points=""/></svg></article>
        <article class="cnc-24__tile" data-key="trial"><p class="cnc-24__k">Trial → paid</p><p class="cnc-24__v" aria-hidden="true">—</p><p class="cnc-24__d"><span></span></p><svg class="cnc-24__spark" viewBox="0 0 120 34" preserveAspectRatio="none" aria-hidden="true"><polyline points=""/></svg></article>
        <article class="cnc-24__tile" data-key="latency"><p class="cnc-24__k">p95 API latency</p><p class="cnc-24__v" aria-hidden="true">—</p><p class="cnc-24__d"><span></span></p><svg class="cnc-24__spark" viewBox="0 0 120 34" preserveAspectRatio="none" aria-hidden="true"><polyline points=""/></svg></article>
      </div>
      <p class="cnc-24__status" role="status" aria-live="polite"></p>
    </div>
    <p class="cnc-24__chip" aria-hidden="true">cached Intl formatters · tween from displayed value · container-query grid · sparkline redraw</p>
  </div>
</section>
.cnc-24,
.cnc-24 *,
.cnc-24 *::before,
.cnc-24 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cnc-24 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --cnc-24-bg: oklch(0.97 0.006 265);
  --cnc-24-card: oklch(1 0 0);
  --cnc-24-ink: oklch(0.24 0.02 265);
  --cnc-24-mut: oklch(0.57 0.02 265);
  --cnc-24-acc: oklch(0.55 0.19 275);
  --cnc-24-up: oklch(0.55 0.14 158);
  --cnc-24-down: oklch(0.58 0.19 25);
  --cnc-24-line: oklch(0 0 0/.08);
  background: var(--cnc-24-bg);
  color: var(--cnc-24-ink);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
}

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

.cnc-24__panel {
  container-type: inline-size;
  width: min(100%,980px);
  padding: clamp(18px,3vw,26px);
  border-radius: 24px;
  background: var(--cnc-24-card);
  box-shadow: 0 1px 2px oklch(0 0 0/.05),0 26px 64px oklch(0.3 0.03 265/.14);
}

.cnc-24__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
}

.cnc-24__title {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -.025em;
}

.cnc-24__sub {
  margin-top: 4px;
  font-size: 12px;
  color: var(--cnc-24-mut);
}

.cnc-24__seg {
  display: flex;
  gap: 2px;
  padding: 3px;
  border-radius: 12px;
  background: oklch(0 0 0/.05);
}

.cnc-24__segb {
  min-height: 34px;
  padding: 7px 15px;
  border: 0;
  border-radius: 9px;
  font: inherit;
  font-size: 12.5px;
  font-weight: 660;
  color: var(--cnc-24-mut);
  background: transparent;
  cursor: pointer;
  transition: background .18s,color .18s,box-shadow .18s;
}

.cnc-24__segb:hover {
  color: var(--cnc-24-ink);
}

.cnc-24__segb:focus-visible {
  outline: 2px solid var(--cnc-24-acc);
  outline-offset: 2px;
}

.cnc-24__segb[aria-pressed="true"] {
  color: var(--cnc-24-ink);
  background: var(--cnc-24-card);
  box-shadow: 0 1px 2px oklch(0 0 0/.12);
}

.cnc-24__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
  margin-top: 20px;
}

@container (min-width: 480px) {
  .cnc-24__grid {
    grid-template-columns: 1fr 1fr;
  }
}

@container (min-width: 780px) {
  .cnc-24__grid {
    grid-template-columns: repeat(3,1fr);
  }
}

.cnc-24__tile {
  position: relative;
  padding: 16px 16px 14px;
  border-radius: 16px;
  border: 1px solid var(--cnc-24-line);
  background: var(--cnc-24-card);
  overflow: hidden;
  transition: border-color .2s,box-shadow .2s,translate .2s;
}

.cnc-24__tile:hover {
  border-color: color-mix(in oklch,var(--cnc-24-acc) 45%,var(--cnc-24-line));
  box-shadow: 0 10px 26px oklch(0.3 0.04 275/.12);
  translate: 0 -2px;
}

.cnc-24__k {
  font-size: 11.5px;
  font-weight: 640;
  color: var(--cnc-24-mut);
}

.cnc-24__v {
  margin-top: 8px;
  font-size: clamp(24px,3.4vw,30px);
  font-weight: 750;
  letter-spacing: -.045em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

.cnc-24__d {
  margin-top: 8px;
  font-size: 11.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.cnc-24__d span {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border-radius: 99px;
  color: var(--cnc-24-up);
  background: color-mix(in oklch,var(--cnc-24-up) 12%,transparent);
}

.cnc-24__tile[data-dir="down"] .cnc-24__d span {
  color: var(--cnc-24-down);
  background: color-mix(in oklch,var(--cnc-24-down) 12%,transparent);
}

.cnc-24__spark {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 58%;
  height: 44px;
  opacity: .9;
}

.cnc-24__spark polyline {
  fill: none;
  stroke: var(--cnc-24-acc);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 260;
  stroke-dashoffset: 0;
  animation: cnc-24-draw .9s cubic-bezier(.16,1,.3,1);
}

.cnc-24__tile[data-dir="down"] .cnc-24__spark polyline {
  stroke: var(--cnc-24-down);
}

@keyframes cnc-24-draw {
  from {
    stroke-dashoffset: 260;
  }

  to {
    stroke-dashoffset: 0;
  }
}

.cnc-24__status {
  margin-top: 16px;
  font-size: 12px;
  color: var(--cnc-24-mut);
}

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

@media (prefers-reduced-motion: reduce) {
  .cnc-24 * {
    animation: none !important;
    transition-duration: .01ms !important;
  }
}
(() => {
  const FMT = {
    usd: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', notation: 'compact', maximumFractionDigits: 1 }),
    num: new Intl.NumberFormat('en-US'),
    publishDate: '2026-05-19T10:00:00.000Z',
    updatedDate: '2026-08-07T16:16:00.000Z',
    pct: new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 1 }),
    ms: new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }),
  };
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const META = {
    mrr: { fmt: 'usd', scale: 1 }, workspaces: { fmt: 'num', scale: 1 }, seats: { fmt: 'num', scale: 1 },
    nrr: { fmt: 'pct', scale: 0.0001 }, trial: { fmt: 'pct', scale: 0.0001 }, latency: { fmt: 'ms', scale: 1, suffix: ' ms', invert: true },
  };
  const DATA = {
    7:  { mrr:[412800,3.1,[18,20,19,24,22,27,30]], workspaces:[1284,1.8,[11,13,12,15,17,16,19]], seats:[9642,2.4,[14,15,17,16,19,21,23]], nrr:[11240,0.6,[20,19,21,22,21,23,24]], trial:[3180,-0.4,[16,18,15,17,14,16,15]], latency:[186,-4.2,[26,24,22,25,21,19,18]] },
    30: { mrr:[398400,8.6,[10,12,15,14,18,17,21,24,23,27,29,32]], workspaces:[1216,6.2,[8,9,11,10,13,15,14,17,18,20,22,24]], seats:[9180,7.4,[9,11,10,14,16,15,18,20,19,23,25,27]], nrr:[11080,1.9,[16,17,16,18,19,18,20,21,20,22,23,24]], trial:[3320,2.1,[12,14,13,16,15,18,17,19,18,20,21,22]], latency:[203,-11.5,[30,29,27,28,25,24,22,23,20,19,18,17]] },
    90: { mrr:[344900,24.3,[6,8,7,11,13,12,16,19,18,23,26,25,29,31,34]], workspaces:[1044,19.8,[5,7,6,9,11,10,13,15,14,18,20,19,23,25,27]], seats:[7960,26.1,[4,6,8,7,11,13,12,16,18,17,21,24,23,27,30]], nrr:[10740,4.4,[13,14,16,15,17,18,17,19,20,19,21,22,21,23,25]], trial:[2960,6.8,[9,11,10,13,12,15,14,17,16,19,18,21,20,22,24]], latency:[241,-18.9,[33,32,30,31,28,27,25,26,23,22,20,21,18,17,16]] },
  };

  document.querySelectorAll('.cnc-24__panel').forEach((panel) => {
    if (panel.dataset.wired) return;
    panel.dataset.wired = '1';
    const status = panel.querySelector('.cnc-24__status');
    const tiles = [...panel.querySelectorAll('.cnc-24__tile')].map((el) => ({
      el, key: el.dataset.key, v: el.querySelector('.cnc-24__v'),
      d: el.querySelector('.cnc-24__d span'), poly: el.querySelector('polyline'), current: 0,
    }));

    const show = (t, raw) => {
      const m = META[t.key];
      t.v.textContent = FMT[m.fmt].format(raw * m.scale) + (m.suffix || '');
    };
    const tween = (t, to, ms) => {
      const from = t.current;
      if (reduce || from === to) { t.current = to; show(t, to); return; }
      const t0 = performance.now();
      const step = (now) => {
        const p = Math.min(1, (now - t0) / ms), e = 1 - Math.pow(1 - p, 4);
        show(t, Math.round(from + (to - from) * e));
        if (p < 1) requestAnimationFrame(step);
      };
      t.current = to;
      requestAnimationFrame(step);
    };
    const redraw = (poly, arr) => {
      const max = Math.max(...arr), min = Math.min(...arr), span = max - min || 1;
      poly.setAttribute('points', arr.map((v, i) =>
        (i / (arr.length - 1) * 116 + 2).toFixed(1) + ',' + (30 - (v - min) / span * 26).toFixed(1)).join(' '));
      poly.style.animation = 'none'; void poly.getBoundingClientRect(); poly.style.animation = '';
    };
    const render = (range) => {
      tiles.forEach((t) => {
        const [val, delta, spark] = DATA[range][t.key];
        const good = META[t.key].invert ? delta < 0 : delta > 0;
        t.el.dataset.dir = good ? 'up' : 'down';
        t.d.textContent = (delta > 0 ? '▲ +' : '▼ ') + delta.toFixed(1) + '%';
        tween(t, val, 900);
        redraw(t.poly, spark);
      });
      status.textContent = 'Showing the last ' + range + ' days. Net MRR ' +
        FMT.usd.format(DATA[range].mrr[0]) + ', ' + DATA[range].mrr[1].toFixed(1) + '% versus the previous period.';
    };

    panel.querySelectorAll('[data-range]').forEach((btn) => {
      btn.addEventListener('click', () => {
        panel.querySelectorAll('[data-range]').forEach((b) => b.setAttribute('aria-pressed', String(b === btn)));
        const go = () => render(btn.dataset.range);
        document.startViewTransition && !reduce ? document.startViewTransition(go) : go();
      });
    });
    render('30');
  });
})();
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: B2B SaaS Analytics Summary Grid
Source: https://codefronts.com/motion/css-number-counter-animations/swiss-saas-analytics/

The summary strip at the top of every B2B analytics product: six metric tiles that re-count whenever the date range changes, each with a delta chip and a sparkline that redraws. One dataset, one render function, one cached formatter per metric — the architecture that stops a dashboard turning into six copies of the same counter code.
## HTML
```html
<section class="cnc-24" aria-label="B2B SaaS analytics summary grid demo">
  <div class="cnc-24__stage">
    <div class="cnc-24__panel">
      <header class="cnc-24__head">
        <div>
          <h3 class="cnc-24__title">Workspace analytics</h3>
          <p class="cnc-24__sub" id="cnc-24-base">Compared with the previous period · sample data</p>
        </div>
        <div class="cnc-24__seg" role="group" aria-label="Date range">
          <button class="cnc-24__segb" type="button" data-range="7" aria-pressed="false">7d</button>
          <button class="cnc-24__segb" type="button" data-range="30" aria-pressed="true">30d</button>
          <button class="cnc-24__segb" type="button" data-range="90" aria-pressed="false">90d</button>
        </div>
      </header>
      <div class="cnc-24__grid">
        <article class="cnc-24__tile" data-key="mrr"><p class="cnc-24__k">Net MRR</p><p class="cnc-24__v" aria-hidden="true">—</p><p class="cnc-24__d"><span></span></p><svg class="cnc-24__spark" viewBox="0 0 120 34" preserveAspectRatio="none" aria-hidden="true"><polyline points=""/></svg></article>
        <article class="cnc-24__tile" data-key="workspaces"><p class="cnc-24__k">Active workspaces</p><p class="cnc-24__v" aria-hidden="true">—</p><p class="cnc-24__d"><span></span></p><svg class="cnc-24__spark" viewBox="0 0 120 34" preserveAspectRatio="none" aria-hidden="true"><polyline points=""/></svg></article>
        <article class="cnc-24__tile" data-key="seats"><p class="cnc-24__k">Paid seats</p><p class="cnc-24__v" aria-hidden="true">—</p><p class="cnc-24__d"><span></span></p><svg class="cnc-24__spark" viewBox="0 0 120 34" preserveAspectRatio="none" aria-hidden="true"><polyline points=""/></svg></article>
        <article class="cnc-24__tile" data-key="nrr"><p class="cnc-24__k">Net revenue retention</p><p class="cnc-24__v" aria-hidden="true">—</p><p class="cnc-24__d"><span></span></p><svg class="cnc-24__spark" viewBox="0 0 120 34" preserveAspectRatio="none" aria-hidden="true"><polyline points=""/></svg></article>
        <article class="cnc-24__tile" data-key="trial"><p class="cnc-24__k">Trial → paid</p><p class="cnc-24__v" aria-hidden="true">—</p><p class="cnc-24__d"><span></span></p><svg class="cnc-24__spark" viewBox="0 0 120 34" preserveAspectRatio="none" aria-hidden="true"><polyline points=""/></svg></article>
        <article class="cnc-24__tile" data-key="latency"><p class="cnc-24__k">p95 API latency</p><p class="cnc-24__v" aria-hidden="true">—</p><p class="cnc-24__d"><span></span></p><svg class="cnc-24__spark" viewBox="0 0 120 34" preserveAspectRatio="none" aria-hidden="true"><polyline points=""/></svg></article>
      </div>
      <p class="cnc-24__status" role="status" aria-live="polite"></p>
    </div>
    <p class="cnc-24__chip" aria-hidden="true">cached Intl formatters · tween from displayed value · container-query grid · sparkline redraw</p>
  </div>
</section>
```
## CSS
```css
.cnc-24,
.cnc-24 *,
.cnc-24 *::before,
.cnc-24 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cnc-24 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  --cnc-24-bg: oklch(0.97 0.006 265);
  --cnc-24-card: oklch(1 0 0);
  --cnc-24-ink: oklch(0.24 0.02 265);
  --cnc-24-mut: oklch(0.57 0.02 265);
  --cnc-24-acc: oklch(0.55 0.19 275);
  --cnc-24-up: oklch(0.55 0.14 158);
  --cnc-24-down: oklch(0.58 0.19 25);
  --cnc-24-line: oklch(0 0 0/.08);
  background: var(--cnc-24-bg);
  color: var(--cnc-24-ink);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
}

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

.cnc-24__panel {
  container-type: inline-size;
  width: min(100%,980px);
  padding: clamp(18px,3vw,26px);
  border-radius: 24px;
  background: var(--cnc-24-card);
  box-shadow: 0 1px 2px oklch(0 0 0/.05),0 26px 64px oklch(0.3 0.03 265/.14);
}

.cnc-24__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
}

.cnc-24__title {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -.025em;
}

.cnc-24__sub {
  margin-top: 4px;
  font-size: 12px;
  color: var(--cnc-24-mut);
}

.cnc-24__seg {
  display: flex;
  gap: 2px;
  padding: 3px;
  border-radius: 12px;
  background: oklch(0 0 0/.05);
}

.cnc-24__segb {
  min-height: 34px;
  padding: 7px 15px;
  border: 0;
  border-radius: 9px;
  font: inherit;
  font-size: 12.5px;
  font-weight: 660;
  color: var(--cnc-24-mut);
  background: transparent;
  cursor: pointer;
  transition: background .18s,color .18s,box-shadow .18s;
}

.cnc-24__segb:hover {
  color: var(--cnc-24-ink);
}

.cnc-24__segb:focus-visible {
  outline: 2px solid var(--cnc-24-acc);
  outline-offset: 2px;
}

.cnc-24__segb[aria-pressed="true"] {
  color: var(--cnc-24-ink);
  background: var(--cnc-24-card);
  box-shadow: 0 1px 2px oklch(0 0 0/.12);
}

.cnc-24__grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
  margin-top: 20px;
}

@container (min-width: 480px) {
  .cnc-24__grid {
    grid-template-columns: 1fr 1fr;
  }
}

@container (min-width: 780px) {
  .cnc-24__grid {
    grid-template-columns: repeat(3,1fr);
  }
}

.cnc-24__tile {
  position: relative;
  padding: 16px 16px 14px;
  border-radius: 16px;
  border: 1px solid var(--cnc-24-line);
  background: var(--cnc-24-card);
  overflow: hidden;
  transition: border-color .2s,box-shadow .2s,translate .2s;
}

.cnc-24__tile:hover {
  border-color: color-mix(in oklch,var(--cnc-24-acc) 45%,var(--cnc-24-line));
  box-shadow: 0 10px 26px oklch(0.3 0.04 275/.12);
  translate: 0 -2px;
}

.cnc-24__k {
  font-size: 11.5px;
  font-weight: 640;
  color: var(--cnc-24-mut);
}

.cnc-24__v {
  margin-top: 8px;
  font-size: clamp(24px,3.4vw,30px);
  font-weight: 750;
  letter-spacing: -.045em;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

.cnc-24__d {
  margin-top: 8px;
  font-size: 11.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

.cnc-24__d span {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border-radius: 99px;
  color: var(--cnc-24-up);
  background: color-mix(in oklch,var(--cnc-24-up) 12%,transparent);
}

.cnc-24__tile[data-dir="down"] .cnc-24__d span {
  color: var(--cnc-24-down);
  background: color-mix(in oklch,var(--cnc-24-down) 12%,transparent);
}

.cnc-24__spark {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 58%;
  height: 44px;
  opacity: .9;
}

.cnc-24__spark polyline {
  fill: none;
  stroke: var(--cnc-24-acc);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 260;
  stroke-dashoffset: 0;
  animation: cnc-24-draw .9s cubic-bezier(.16,1,.3,1);
}

.cnc-24__tile[data-dir="down"] .cnc-24__spark polyline {
  stroke: var(--cnc-24-down);
}

@keyframes cnc-24-draw {
  from {
    stroke-dashoffset: 260;
  }

  to {
    stroke-dashoffset: 0;
  }
}

.cnc-24__status {
  margin-top: 16px;
  font-size: 12px;
  color: var(--cnc-24-mut);
}

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

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

## JavaScript
```js
(() => {
  const FMT = {
    usd: new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', notation: 'compact', maximumFractionDigits: 1 }),
    num: new Intl.NumberFormat('en-US'),
    publishDate: '2026-05-19T10:00:00.000Z',
    updatedDate: '2026-08-07T16:16:00.000Z',
    pct: new Intl.NumberFormat('en-US', { style: 'percent', maximumFractionDigits: 1 }),
    ms: new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }),
  };
  const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
  const META = {
    mrr: { fmt: 'usd', scale: 1 }, workspaces: { fmt: 'num', scale: 1 }, seats: { fmt: 'num', scale: 1 },
    nrr: { fmt: 'pct', scale: 0.0001 }, trial: { fmt: 'pct', scale: 0.0001 }, latency: { fmt: 'ms', scale: 1, suffix: ' ms', invert: true },
  };
  const DATA = {
    7:  { mrr:[412800,3.1,[18,20,19,24,22,27,30]], workspaces:[1284,1.8,[11,13,12,15,17,16,19]], seats:[9642,2.4,[14,15,17,16,19,21,23]], nrr:[11240,0.6,[20,19,21,22,21,23,24]], trial:[3180,-0.4,[16,18,15,17,14,16,15]], latency:[186,-4.2,[26,24,22,25,21,19,18]] },
    30: { mrr:[398400,8.6,[10,12,15,14,18,17,21,24,23,27,29,32]], workspaces:[1216,6.2,[8,9,11,10,13,15,14,17,18,20,22,24]], seats:[9180,7.4,[9,11,10,14,16,15,18,20,19,23,25,27]], nrr:[11080,1.9,[16,17,16,18,19,18,20,21,20,22,23,24]], trial:[3320,2.1,[12,14,13,16,15,18,17,19,18,20,21,22]], latency:[203,-11.5,[30,29,27,28,25,24,22,23,20,19,18,17]] },
    90: { mrr:[344900,24.3,[6,8,7,11,13,12,16,19,18,23,26,25,29,31,34]], workspaces:[1044,19.8,[5,7,6,9,11,10,13,15,14,18,20,19,23,25,27]], seats:[7960,26.1,[4,6,8,7,11,13,12,16,18,17,21,24,23,27,30]], nrr:[10740,4.4,[13,14,16,15,17,18,17,19,20,19,21,22,21,23,25]], trial:[2960,6.8,[9,11,10,13,12,15,14,17,16,19,18,21,20,22,24]], latency:[241,-18.9,[33,32,30,31,28,27,25,26,23,22,20,21,18,17,16]] },
  };

  document.querySelectorAll('.cnc-24__panel').forEach((panel) => {
    if (panel.dataset.wired) return;
    panel.dataset.wired = '1';
    const status = panel.querySelector('.cnc-24__status');
    const tiles = [...panel.querySelectorAll('.cnc-24__tile')].map((el) => ({
      el, key: el.dataset.key, v: el.querySelector('.cnc-24__v'),
      d: el.querySelector('.cnc-24__d span'), poly: el.querySelector('polyline'), current: 0,
    }));

    const show = (t, raw) => {
      const m = META[t.key];
      t.v.textContent = FMT[m.fmt].format(raw * m.scale) + (m.suffix || '');
    };
    const tween = (t, to, ms) => {
      const from = t.current;
      if (reduce || from === to) { t.current = to; show(t, to); return; }
      const t0 = performance.now();
      const step = (now) => {
        const p = Math.min(1, (now - t0) / ms), e = 1 - Math.pow(1 - p, 4);
        show(t, Math.round(from + (to - from) * e));
        if (p < 1) requestAnimationFrame(step);
      };
      t.current = to;
      requestAnimationFrame(step);
    };
    const redraw = (poly, arr) => {
      const max = Math.max(...arr), min = Math.min(...arr), span = max - min || 1;
      poly.setAttribute('points', arr.map((v, i) =>
        (i / (arr.length - 1) * 116 + 2).toFixed(1) + ',' + (30 - (v - min) / span * 26).toFixed(1)).join(' '));
      poly.style.animation = 'none'; void poly.getBoundingClientRect(); poly.style.animation = '';
    };
    const render = (range) => {
      tiles.forEach((t) => {
        const [val, delta, spark] = DATA[range][t.key];
        const good = META[t.key].invert ? delta < 0 : delta > 0;
        t.el.dataset.dir = good ? 'up' : 'down';
        t.d.textContent = (delta > 0 ? '▲ +' : '▼ ') + delta.toFixed(1) + '%';
        tween(t, val, 900);
        redraw(t.poly, spark);
      });
      status.textContent = 'Showing the last ' + range + ' days. Net MRR ' +
        FMT.usd.format(DATA[range].mrr[0]) + ', ' + DATA[range].mrr[1].toFixed(1) + '% versus the previous period.';
    };

    panel.querySelectorAll('[data-range]').forEach((btn) => {
      btn.addEventListener('click', () => {
        panel.querySelectorAll('[data-range]').forEach((b) => b.setAttribute('aria-pressed', String(b === btn)));
        const go = () => render(btn.dataset.range);
        document.startViewTransition && !reduce ? document.startViewTransition(go) : go();
      });
    });
    render('30');
  });
})();
```

How this works

Formatters are expensive; build them once, outside every loop:

const FMT = {                                    // cached at module scope
  usd: new Intl.NumberFormat('en-US',{style:'currency',currency:'USD',notation:'compact'}),
  num: new Intl.NumberFormat('en-US'),
  pct: new Intl.NumberFormat('en-US',{style:'percent',maximumFractionDigits:1}),
  ms:  new Intl.NumberFormat('en-US',{maximumFractionDigits:0}),
};

Constructing an Intl.NumberFormat inside a rAF frame is the single most common performance bug in animated dashboards — it allocates and re-parses locale data 60 times a second per tile. Build the formatter once, call .format() in the loop.

const render = (range) => {
  tiles.forEach((t) => {
    const next = DATA[range][t.key];
    tween(t.el, t.current, next.v, 900, FMT[t.fmt]);   // tween FROM the current value
    t.current = next.v;
    redraw(t.spark, next.spark);
  });
};

Each tile tweens from whatever it is currently showing to the new value, so switching 7d → 90d → 30d reads as a continuous morph rather than a reset. The sparkline sets new points and restarts its draw animation with the reflow-flush trick (el.style.animation='none'; void el.offsetWidth; el.style.animation='') because SVG points is not a CSS-animatable property. The grid itself is a container query layout — it reflows to the tile strip's own width, so the same component works in a sidebar, a modal or full bleed.

Make it yours

  • Add a metric: push one object into DATA and one tile into the markup — the render loop is data-driven and needs no change.
  • Trend colouring: derive the delta chip's hue from the sign with color-mix(in oklch, var(--up) calc(var(--d) * 1%), var(--down)) for a continuous scale instead of two states.
  • Cross-fade on range switch: wrap the render call in document.startViewTransition?.() — supported browsers get a smooth morph, others get the tween.
  • Server data: swap DATA for a fetch keyed by range and cache responses; keep the tween so the numbers never jump on arrival.

Gotchas — read before shipping

  • Never construct Intl formatters (or Date objects) inside the animation frame — cache them at module scope.
  • Tween from the DISPLAYED value, not from zero, on every range change; counting from 0 each time makes a dashboard feel like it is reloading.
  • SVG points cannot be CSS-transitioned. Either restart a stroke-dashoffset draw (this demo) or interpolate the point array yourself in JS.
  • Six aria-live tiles all announcing at once is chaos — put one polite live region on the summary sentence and mark the tile digits aria-hidden with a static value in the accessible name.
  • A percentage delta needs its baseline in the DOM ('vs previous 30 days'), or the number is unfalsifiable and users stop trusting the dashboard.

Browser support

ChromeSafariFirefoxEdge
111+16.4+110+111+

Container queries (Chrome 105 / Safari 16 / Firefox 110) drive the responsive grid; without them the tiles stack in one column, which is still correct. Intl compact notation is supported everywhere current.

Search CodeFronts

Loading…