Remotion LabRemotion Lab
核心動畫KPI 計數卡片:數字從 0 跳到目標值
data-vizkpicount-upinterpolateintermediate

KPI 計數卡片:數字從 0 跳到目標值

用 interpolate + ease-out 做出儀表板上常見的 KPI 計數動畫。三張卡片 stagger 進場、tabular-nums 解決數字跳動、計數完成後彈跳一下——數據儀表板最基本也最有效的元件。

成品預覽

6 秒、三張並排的 KPI 卡:營收 NT$1,248 萬、訂單數 3,240、客單價 NT$3,803。每張卡片數字從 0 計到目標值,stagger 進場、計數完成時整張卡彈跳一下、右下角顯示年增率。整支影片只用了 interpolate + 一行 ease-out 公式,沒有任何外部圖表庫。


這篇教什麼

KPI 卡片是儀表板影片最常見也最簡單的元件,但要做得「看起來專業」有幾個關鍵:

  1. 計數的節奏——線性插值看起來很機械,要用 ease-out 讓尾段慢下來
  2. 數字不要跳動——font-variant-numeric: tabular-nums 是這個畫面能不能看的關鍵
  3. 進場 stagger——三張卡同時出來會很擠,每張間隔幾 frame 看起來才有「節奏感」
  4. 完成的反饋——計到目標值時整張卡彈跳一下,讓觀眾「知道結束了」

這四個技巧加起來就是 90% 的儀表板數字動畫。後面三篇會把這個做法套到長條圖、圓餅圖、折線圖。


前置知識


Step 1:資料先建好,畫面後做

// src/data/q1-2026.ts
export const KPIS = [
  { label: "營收",   value: 12_480_000, prefix: "NT$", unit: "萬", divisor: 10_000 },
  { label: "訂單數", value: 3_240,      prefix: "",    unit: "",   divisor: 1 },
  { label: "客單價", value: 3_803,      prefix: "NT$", unit: "",   divisor: 1 },
];

divisor 是給「營收」這種大數字用的——12,480,000 顯示成「1,248 萬」比較好讀。把資料跟畫面分離是數據動畫的關鍵:之後想換成 Q2 的數據,只要改 q2-2026.ts,畫面完全不用動。這是 Remotion 相比 After Effects 的根本優勢。


Step 2:計數公式 + ease-out

每張卡的核心邏輯是「在 60 frame 內從 0 走到目標值,但走得不要太機械」。

import {interpolate, useCurrentFrame} from 'remotion';
 
const KpiCard: React.FC<{kpi: typeof KPIS[number]; delay: number}> = ({kpi, delay}) => {
  const frame = useCurrentFrame();
  const localFrame = Math.max(0, frame - delay);
 
  // 0 → 1 線性
  const progress = interpolate(localFrame, [0, 60], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  // ease-out(cubic):頭快尾慢
  const eased = 1 - Math.pow(1 - progress, 3);
 
  // 計數中的當前值
  const current = Math.round((kpi.value / kpi.divisor) * eased);
  const display = current.toLocaleString("zh-TW");
 
  return (
    <div style={cardStyle}>
      <div style={{fontSize: 30, color: "#94a3b8"}}>{kpi.label}</div>
      <div style={{display: "flex", alignItems: "baseline"}}>
        <span style={numberStyle}>{kpi.prefix}{display}</span>
        {kpi.unit && <span style={unitStyle}>{kpi.unit}</span>}
      </div>
    </div>
  );
};

為什麼要 ease-out? 線性 interpolate 讓 0→1248 等速跑完,看起來像進度條而不是「數字跳動」。1 - (1 - t)^3 是 cubic ease-out,數學上等同於 t * (3 - 3t + t²)——前 1/3 跑完 70%,後 2/3 慢慢逼近目標值。觀眾的視覺會被前段的快速跳動抓住,再被後段的緩慢逼近「拉到目標值」。這就是「數字計數很有戲劇感」的秘密。


Step 3:tabular-nums 解決數字跳動

如果你照上面寫完跑一次,會發現一個惱人的問題——數字在計數過程中會輕微抖動。原因是大部分字型的數字字元寬度不固定(proportional figures),「1」很窄、「8」很寬,所以「128」變成「188」時整個字串會橫向位移幾個 pixel。

解法只有一行:

const numberStyle = {
  fontSize: 60,
  fontWeight: 800,
  color: "#fff",
  fontVariantNumeric: "tabular-nums" as const,  // ← 這行
  letterSpacing: "-0.02em",
  whiteSpace: "nowrap" as const,
};

tabular-nums 強制所有數字字元寬度一致(通常是 0~9 都用 8 的寬度)。Inter、Noto Sans、SF Pro 都支援這個 OpenType feature。沒有這一行,你的 KPI 數字看起來就會像便宜的 stock template


Step 4:Stagger 進場 + 完成彈跳

三張卡同時出現會很擠。用 delay 參數讓每張卡晚一點進場:

{KPIS.map((kpi, i) => (
  <KpiCard key={kpi.label} kpi={kpi} delay={i * 12} />
))}

每張卡延遲 12 frames(0.4 秒)。第一張先彈進來、開始計數;觀眾的眼睛還在第一張時,第二張進場;以此類推。這是視覺敘事的基本功——所有東西同時動 = 觀眾不知道看哪 = 訊息流失

進場本身用 spring 做 scale + opacity:

import {spring, useVideoConfig} from 'remotion';
 
const {fps} = useVideoConfig();
const enter = spring({
  frame: localFrame,
  fps,
  config: {damping: 14, stiffness: 120},
  durationInFrames: 30,
});
const scale = interpolate(enter, [0, 1], [0.8, 1]);
const opacity = enter;

計到目標值時讓整張卡再彈一下:

// 第 60 frame 計數結束,60-72 之間做一個小彈跳
const bounce = interpolate(localFrame, [60, 66, 72], [1, 1.08, 1], {
  extrapolateLeft: "clamp",
  extrapolateRight: "clamp",
});
const finalScale = scale * bounce;

這個小彈跳 = 計數結束的視覺反饋。觀眾的潛意識會認知「啊,數字停了」。沒有這個動作,計數會「偷偷」結束,少一個情緒節點。


Step 5:渲染

npx remotion render KpiCounterCards out/kpi-cards.mp4 --codec=h264

常見問題

Q:數字計數中途看起來「跳格」很明顯?

通常是因為你用 Math.floor 而不是 Math.round,或目標數字不夠大。如果目標是 35,那 60 frame 內只會跳 35 次,每兩 frame 才跳一格——看起來確實像在跳。解法:把目標換成更大的數字(比如顯示百分比 × 100),或者用 toFixed(1) 顯示一位小數讓「補間」更平滑。

Q:「萬」字會跑到下一行?

字級太大時 flex baseline 會被換行。把主字級從 76 降到 60、單位從 36 降到 30 通常就解決了。如果不想動字級,可以用 flexWrap: "nowrap" + whiteSpace: "nowrap" 雙保險。

Q:要顯示貨幣 NT$12,480,000 怎麼辦?

new Intl.NumberFormat("zh-TW", {
  style: "currency",
  currency: "TWD",
  maximumFractionDigits: 0,
}).format(12_480_000);
// → "NT$12,480,000"

但格式化字串會比手刻 prefix + toLocaleString 慢一點,每幀調用 60 次也沒問題,只是 GC 壓力略高。預算緊張的話用手刻,否則 Intl.NumberFormat 比較不會出國際化的 bug。

Q:可以在計數過程中改變字體顏色嗎?

可以,用 interpolateColors

import {interpolateColors} from 'remotion';
const color = interpolateColors(eased, [0, 1], ["#94a3b8", "#10b981"]);

從灰色慢慢變成綠色(成長指標的視覺暗示)。


本篇涵蓋的官方文件


下一步

T7:動態長條圖:spring 驅動的高度成長 — 把 KPI 數字延伸到「比較型」視覺。下一篇你會看到怎麼把 N 個資料點同時用 spring 動畫成長,配上 stagger 做出「依序長出來」的經典效果。