Something new is coming.Join the waitlist

Animated Counter

PreviousNext

React count-up number animation that triggers on scroll into view. Supports decimals, locale formatting, and custom easing via Framer Motion. Install with shadcn CLI.

0
Active users
0.00%
Avg. uptime
+0
Built with tent ui

Installation

npx shadcn@latest add https://tentui.com/r/animated-counter.json

Usage

import { AnimatedCounter } from "@/components/animated-counter";
 
export function HeroStat() {
  return (
    <p className="text-4xl font-semibold">
      <AnimatedCounter value={12480} /> users
    </p>
  );
}

The counter holds the starting value until it scrolls into view (50% visible). Then it animates to value over duration seconds and stays put — once is the default. Pass once={false} if you want it to replay every time it re-enters the viewport.

Patterns

Locale formatting

The default rendering is plain toFixed(decimals). For thousands separators or currency formatting, pass a format function.

<AnimatedCounter
  value={12480}
  format={(v) => v.toLocaleString("en-US")}
/>
 
<AnimatedCounter
  value={1299}
  decimals={2}
  format={(v) =>
    v.toLocaleString("en-US", {
      style: "currency",
      currency: "USD"
    })
  }
/>

Decimals (uptime, ratings, percentages)

<AnimatedCounter value={99.98} decimals={2} />%
<AnimatedCounter value={4.7} decimals={1} /> / 5

Replay on re-enter

<AnimatedCounter value={500} once={false} />

Props

PropTypeDefaultDescription
valuenumberFinal value to animate to. Required.
fromnumber0Starting value before the animation begins.
durationnumber1.6Animation duration in seconds.
decimalsnumber0Decimal places to render when no format function is supplied.
format(value: number) => stringCustom formatter — overrides decimals. Use for locale or currency output.
oncebooleantrueRun only the first time the counter scrolls into view.
ease"linear" | "easeIn" | "easeOut" | "easeInOut""easeOut"Easing curve passed to animate().

Performance & accessibility

  • The animation only starts when the element is in the viewport (via useInView with a 50% threshold), so a long page with many counters won't kick off all animations on mount.
  • tabular-nums is set on the rendered span so digits don't shift left/right as values change — avoids layout jitter at higher values.
  • The visible text is the live value, so screen readers and copy/paste both see the final number after the animation settles. If you need an immediate, announceable value, render the final number in a visually hidden sibling.