Metric Card

A polished card that counts up to a big number with a label and sublabel — perfect for 'by the numbers' sections.

Preview

Open editor

Usage

Big numbers shouldn't appear — they should count up. Drop in a value, optional prefix ($) and suffix (x, %, K, M), and a label. The number animates from 0 to the target with an ease-out curve over ~36 frames.

Use these for "10x faster", "$50M saved", "99.9% uptime" — every launch deck has 3 of these.

Props

NameTypeDefault
valuenumber10
prefixstring""
suffixstring"x"
labelstring"faster than ever"
sublabelstring"Average response time vs. last quarter"
theme"light" | "dark""light"

Composition

ID
MetricCard
Resolution
1280×720
FPS
60
Duration
1.7s

Source

Copy or download the React source — drop it into your own Remotion project. The only runtime dependency is remotion.

"use client";
import {
  AbsoluteFill,
  Easing,
  interpolate,
  spring,
  useVideoConfig,
} from "remotion";
import { type ClipStyle, resolveClipStyle } from "../../clip-style";
import { snap } from "../../snap";
import { useDesignFrame } from "../../use-design-frame";

export type MetricCardProps = {
  value: number;
  prefix: string;
  suffix: string;
  label: string;
  sublabel: string;
  theme: "light" | "dark";
  clipStyle?: ClipStyle;
};

const D_CARD = 0;
const D_NUMBER = 8;
const COUNT_DURATION = 36;
const D_LABEL = 18;
const D_SUBLABEL = 26;
const COUNT_EASE = Easing.bezier(0.16, 1, 0.3, 1);

function formatNumber(n: number): string {
  const rounded = Math.round(n);
  return rounded.toLocaleString();
}

export const MetricCard: React.FC<MetricCardProps> = ({
  value,
  prefix,
  suffix,
  label,
  sublabel,
  theme,
  clipStyle,
}) => {
  const frame = useDesignFrame();
  const { fps } = useVideoConfig();
  const isDark = theme === "dark";
  const s = resolveClipStyle(clipStyle, {
    background: "#f7f7f9",
    color: isDark ? "#ffffff" : "#0f1014",
    fontFamily:
      "-apple-system, BlinkMacSystemFont, 'SF Pro Display', Inter, sans-serif",
    accent: "#6366f1",
  });
  const accent = s.accent;
  const bg = s.background;
  const fontFamily = s.fontFamily;

  const cardBg = isDark ? "#15161A" : "#ffffff";
  const text = isDark ? "#ffffff" : "#0f1014";
  const muted = isDark ? "rgba(255,255,255,0.55)" : "rgba(15,16,20,0.55)";
  const border = isDark ? "rgba(255,255,255,0.08)" : "rgba(15,16,20,0.08)";

  const cardPop = spring({
    frame: frame - D_CARD,
    fps,
    config: { damping: 16, stiffness: 110, mass: 0.8 },
  });

  const countProgress = interpolate(
    frame,
    [D_NUMBER, D_NUMBER + COUNT_DURATION],
    [0, 1],
    {
      extrapolateLeft: "clamp",
      extrapolateRight: "clamp",
      easing: COUNT_EASE,
    },
  );
  const animatedValue = value * countProgress;

  const numberPop = spring({
    frame: frame - D_NUMBER,
    fps,
    config: { damping: 13, stiffness: 160, mass: 0.6 },
  });

  return (
    <AbsoluteFill
      style={{
        background: bg,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        fontFamily,
      }}
    >
      <div
        style={{
          width: 760,
          background: cardBg,
          border: `1px solid ${border}`,
          borderRadius: 32,
          padding: "60px 56px",
          textAlign: "center",
          backgroundImage: `radial-gradient(120% 80% at 50% 0%, ${accent}22, transparent 70%)`,
          boxShadow: isDark
            ? "0 30px 80px rgba(0,0,0,0.45)"
            : "0 30px 80px rgba(15,16,20,0.08)",
          opacity: cardPop,
          transform: `translate3d(0, ${snap((1 - cardPop) * 24)}px, 0) scale(${0.95 + cardPop * 0.05})`,
        }}
      >
        <div
          style={{
            display: "flex",
            alignItems: "baseline",
            justifyContent: "center",
            gap: 4,
            color: accent,
            fontSize: 132,
            fontWeight: 800,
            letterSpacing: "-0.04em",
            lineHeight: 1,
            opacity: numberPop,
            transform: `scale(${0.85 + numberPop * 0.15})`,
            fontVariantNumeric: "tabular-nums",
          }}
        >
          {prefix ? (
            <span style={{ fontSize: 80, fontWeight: 700 }}>{prefix}</span>
          ) : null}
          <span>{formatNumber(animatedValue)}</span>
          {suffix ? (
            <span style={{ fontSize: 80, fontWeight: 700 }}>{suffix}</span>
          ) : null}
        </div>

        <RevealItem frame={frame - D_LABEL} fps={fps}>
          <div
            style={{
              fontSize: 32,
              fontWeight: 600,
              color: text,
              letterSpacing: "-0.01em",
              marginTop: 18,
            }}
          >
            {label}
          </div>
        </RevealItem>

        <RevealItem frame={frame - D_SUBLABEL} fps={fps}>
          <div
            style={{
              fontSize: 18,
              color: muted,
              fontWeight: 400,
              marginTop: 8,
              letterSpacing: "-0.005em",
            }}
          >
            {sublabel}
          </div>
        </RevealItem>
      </div>
    </AbsoluteFill>
  );
};

function RevealItem({
  frame,
  fps,
  children,
}: {
  frame: number;
  fps: number;
  children: React.ReactNode;
}) {
  const reveal = spring({
    frame,
    fps,
    config: { damping: 14, stiffness: 150, mass: 0.7 },
  });
  return (
    <div
      style={{
        opacity: reveal,
        transform: `translate3d(0, ${snap((1 - reveal) * 12)}px, 0)`,
      }}
    >
      {children}
    </div>
  );
}
Save as MetricCard/MetricCard.tsx