Staaarter

Animated Counter Row

A row of stats, visually similar to a simple stat row, but each number animates counting up from 0 to its target value once the block mounts. Uses a fixed-step setInterval loop rather than scroll-triggering.

By Staaarter Team
Stats
Docs
Live preview

Docs

Installation

A real shadcn registry command, not a copy-paste stand-in: it fetches this block plus its dependencies and any missing npm packages, and installs them straight into your project.

npx shadcn add https://staaarter.com/r/animated-counter-row.json

Prefer not to use the CLI? Copy the source from the Code toggle above into src/components/blocks/stats/stats5.tsx instead.

Usage

The file also exports stats5Demo, the exact props behind the preview above. Spread it to get a working section in one line, then replace it with your own data.

import { Stats5, stats5Demo } from "@/components/blocks/stats/stats5";

export default function Page() {
  return <Stats5 {...stats5Demo} />;
}

Props

PropTypeDefaultDescription
headingstringundefinedOptional heading above the stat row.
descriptionstringundefinedOptional supporting text under the heading.
statsCounterStat[]noneThe stat entries; each value is the animation's target number.
classNamestringnoneExtra classes for the outer section element.

Types

type CounterStat = {
  value: number;
  label: string;
  prefix?: string; // e.g. "$"
  suffix?: string; // e.g. "%" or "+"
};

Behavior notes

  • The count-up is real, local-state interactivity: a useCountUp hook runs a fixed 24-step setInterval (30ms per step) started in a useEffect on mount, incrementing every number from 0 toward its target in lockstep. This is the same pattern used by the Hero category's hero1 counters.
  • The animation is intentionally NOT scroll-triggered. An earlier attempt used IntersectionObserver to start the count-up only when the block scrolled into view, but this block's live preview renders inside an iframe via a React portal (the component's JS runs in the parent window while its DOM lives in the iframe's document), and the observer's implicit root resolved against the wrong window and never fired, even with an explicit root element. Animating unconditionally on mount is the fix that reliably works in that architecture.
  • Because it's mount-triggered rather than scroll-triggered, the counters animate immediately every time the block is rendered, including in the category grid's and sibling-list's card thumbnails, not just on a full page load.
  • The demo props live in a separate stats5.demo.tsx sibling file (not exported from the "use client" component file itself), since a demo-props object exported from a client module resolves to undefined when read from the server-rendered preview shell.