Catalog

Edit Props (live)

TEXT
TEXT
config
config
config
config
config
config
config

Source

136 lines
// Animated Stat Counter — a big number that counts up from zero (ease-out)
// with a label and optional prefix/suffix. Native Remotion, frame-driven.
// Ubiquitous in marketing/finance content; trivial, looks expensive.

import {
  AbsoluteFill,
  Easing,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
} from 'remotion';
import { loadFont as loadInter } from '@remotion/google-fonts/Inter';
import { loadFont as loadGrotesk } from '@remotion/google-fonts/SpaceGrotesk';

const { fontFamily: GROTESK } = loadGrotesk();
const { fontFamily: INTER } = loadInter();
const FONT_MAP: Record<string, string> = {
  SpaceGrotesk: GROTESK,
  Inter: INTER,
};

export type AnimatedStatCounterProps = {
  value: number | string;
  label: string;
  prefix: string;
  suffix: string;
  decimals: number;
  accentColor: string;
  textColor: string;
  backgroundColor: string;
  font: 'SpaceGrotesk' | 'Inter';
};

export default function AnimatedStatCounter({
  value = 1000,
  label = 'Active users',
  prefix = '',
  suffix = '',
  decimals = 0,
  accentColor = '#22c55e',
  textColor = '#ffffff',
  backgroundColor = '#0a0a14',
  font = 'SpaceGrotesk',
}: Partial<AnimatedStatCounterProps>) {
  const frame = useCurrentFrame();
  const { fps, durationInFrames, width } = useVideoConfig();
  // Resolution-independent: px authored at a 1080-wide reference, scaled
  // by k. k=1 at 1080 (identical output), scales cleanly at any width.
  const k = width / 1080;
  const family = FONT_MAP[font] ?? GROTESK;
  const target = Number(value) || 0;
  const dec = Math.max(0, Math.min(4, Number(decimals) || 0));

  const countEnd = Math.min(durationInFrames - 10, Math.round(fps * 1.5));
  const t = interpolate(frame, [8, countEnd], [0, 1], {
    extrapolateLeft: 'clamp',
    extrapolateRight: 'clamp',
    easing: Easing.out(Easing.cubic),
  });
  const current = target * t;
  const display =
    prefix +
    current.toLocaleString(undefined, {
      minimumFractionDigits: dec,
      maximumFractionDigits: dec,
    }) +
    suffix;

  const popIn = spring({
    frame,
    fps,
    config: { damping: 13 },
    durationInFrames: 20,
  });
  const exit = interpolate(
    frame,
    [durationInFrames - 10, durationInFrames - 1],
    [1, 0],
    { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }
  );
  const labelOpacity = interpolate(
    frame,
    [countEnd - 6, countEnd + 8],
    [0, 1],
    {
      extrapolateLeft: 'clamp',
      extrapolateRight: 'clamp',
    }
  );

  return (
    <AbsoluteFill
      style={{
        backgroundColor,
        justifyContent: 'center',
        alignItems: 'center',
        opacity: exit,
        fontFamily: family,
      }}
    >
      <div
        style={{
          textAlign: 'center',
          transform: `scale(${interpolate(popIn, [0, 1], [0.72, 1])})`,
          opacity: popIn,
        }}
      >
        <div
          style={{
            fontSize: 220 * k,
            fontWeight: 900,
            color: accentColor,
            letterSpacing: '-0.03em',
            lineHeight: 1,
            fontVariantNumeric: 'tabular-nums',
          }}
        >
          {display}
        </div>
        <div
          style={{
            marginTop: 28 * k,
            fontSize: 50 * k,
            fontWeight: 600,
            color: textColor,
            opacity: labelOpacity,
          }}
        >
          {label}
        </div>
      </div>
    </AbsoluteFill>
  );
}

Schema (Zod)

import { z } from 'zod';

export const Schema = z.object({
  value: z
    .union([z.number(), z.string()])
    .default(1000)
    .describe('$port:text The target number (counts up from zero)'),
  label: z
    .string()
    .default('Active users')
    .describe('$port:text Label under the number'),
  prefix: z.string().default('').describe('Prefix, e.g. $ (optional)'),
  suffix: z.string().default('').describe('Suffix, e.g. % or K (optional)'),
  decimals: z.number().default(0).describe('$style:size Decimal places (0–4)'),
  accentColor: z
    .string()
    .default('#22c55e')
    .describe('$style:color Number color'),
  textColor: z.string().default('#ffffff').describe('$style:color Label color'),
  backgroundColor: z
    .string()
    .default('#0a0a14')
    .describe('$style:color Background color'),
  font: z
    .enum(['SpaceGrotesk', 'Inter'])
    .default('SpaceGrotesk')
    .describe('$style:font Font'),
});

Animated Stat Counter

animated_stat_counter

A big number that counts up from zero with an ease-out, plus a label and optional prefix/suffix. Use for any stat, metric, price, or count reveal.

Workflow Inputs (2)

  • The target number (counts up from zero)
    value
    TEXT
  • Label under the number
    label
    TEXT

Config Fields (7)

  • font
  • prefix
  • suffix
  • decimals
  • textColor
  • accentColor
  • backgroundColor

Meta

Updated
7/10/2026