公開日: 2026/01/11
個人開発をしていると、数字の変化を肌で感じることがモチベーション維持に重要だと感じています。
「MAU 23人」「月間PV 212」「月間収益 ¥0」——これらの数字が静止したまま表示されるのと、0からカウントアップされるのとでは、見た目のワクワク感が全く違いました。
後者の方が「サービスが動いている」「データが更新されている」という実感が湧き、ページを開いた時の嬉しさがアップします。
今回は、Next.jsで構築したdash boardに、react-countupを使って数値のカウントアップアニメーションを実装した記録を残します。
数値のカウントアップアニメーションを実装するライブラリはいくつかありますが、今回は**react-countup**を選択しました。
選んだ理由:
npm install react-countupこれだけで準備完了です。
実装時に直面した課題は、スプレッドシートから取得したデータが「¥1,000」や「23 人」のように単位付きの文字列として渡ってくることでした。
// 実際のデータ例
value={data.mau.toLocaleString()} // "23"
value={`¥${data.mr.toLocaleString()}`} // "¥0"
react-countupのCountUpコンポーネントは、endプロパティに数値を渡す必要があります。そのため、文字列のままではアニメーションできません。
この課題を解決するため、正規表現で数値部分のみを抽出し、prefix(数値の前の文字)とsuffix(数値の後の文字)を分離する関数を作成しました。
/**
* 文字列から数値部分を抽出し、prefix/suffixを分離する
* @param value 文字列または数値
* @returns { numericValue: number, prefix: string, suffix: string }
*/
function parseValue(value: string | number): {
numericValue: number;
prefix: string;
suffix: string;
} {
// 数値の場合はそのまま返す
if (typeof value === "number") {
return { numericValue: value, prefix: "", suffix: "" };
}
// 文字列の場合、数値部分を抽出
const numericMatch = value.match(/[\d,]+/);
if (!numericMatch) {
return { numericValue: 0, prefix: value, suffix: "" };
}
const numericValue = parseFloat(numericMatch[0].replace(/,/g, ""));
// prefix(数値の前の文字)を抽出
const prefixMatch = value.match(/^[^\d]*/);
const prefix = prefixMatch ? prefixMatch[0] : "";
// suffix(数値の後の文字)を抽出
const numericIndex = numericMatch.index ?? 0;
const numericEndIndex = numericIndex + numericMatch[0].length;
const suffix = value.substring(numericEndIndex);
return { numericValue, prefix, suffix };
}
この関数の動作例:
"23" → { numericValue: 23, prefix: "", suffix: "" }"¥1,200" → { numericValue: 1200, prefix: "¥", suffix: "" }"23 人" → { numericValue: 23, prefix: "", suffix: " 人" }これにより、見た目の整合性(単位や記号の表示)とアニメーションを両立できました。
実装前のコードは、シンプルに数値を表示するだけでした。
export function KPICard({
title,
value,
description,
metric,
isSelected,
onClick,
}: KPICardProps) {
return (
<Card>
<CardContent className="p-6">
<p className="text-sm font-medium text-slate-600">{title}</p>
<p className="mt-2 text-3xl font-bold text-slate-900">{value}</p>
{description && (
<p className="mt-1 text-xs text-slate-500">{description}</p>
)}
</CardContent>
</Card>
);
}
問題点:
実装後のコードでは、CountUpコンポーネントを使用して、数値が0から目標値までカウントアップされます。
import CountUp from "react-countup";
export function KPICard({
title,
value,
description,
metric,
isSelected,
onClick,
}: KPICardProps) {
const { numericValue, prefix, suffix } = parseValue(value);
return (
<Card>
<CardContent className="p-6">
<p className="text-sm font-medium text-slate-600">{title}</p>
<p className="mt-2 text-3xl font-bold text-slate-900">
{prefix}
<CountUp
start={0}
end={numericValue}
duration={2.5}
separator=","
useEasing={true}
/>
{suffix}
</p>
{description && (
<p className="mt-1 text-xs text-slate-500">{description}</p>
)}
</CardContent>
</Card>
);
}
実装のポイント:
start={0}: 0から開始することで、数値が「生まれる」感覚を演出end={numericValue}: 抽出した数値までカウントアップduration={2.5}: 2.5秒かけてアニメーション(臨場感を出すため少し長めに設定)separator=",": 3桁区切りのカンマを自動で挿入(例: 1,234)useEasing={true}: イージングを有効化(最初は速く、最後はゆっくり止まる)useEasing={true}を設定することで、数値が自然に止まる動きになります。
この「心地よい止まり方」が、ダッシュボードの体験を大きく向上させます。
実装後のダッシュボードでは、以下のように動作します:
0 → 23 までカウントアップ(2.5秒)0 → 212 までカウントアップ(2.5秒)¥0 → ¥1,200 までカウントアップ(2.5秒、¥は固定表示)0 → 2 までカウントアップ(2.5秒)ページを読み込むたびに、すべての数値が同時にカウントアップされるため、視覚的にインパクトがあります。
参考までに、完全な実装コードを掲載します。
"use client";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/src/lib/utils";
import CountUp from "react-countup";
interface KPICardProps {
title: string;
value: string | number;
description?: string;
metric: "mau" | "pv" | "mr";
isSelected: boolean;
onClick: () => void;
}
/**
* 文字列から数値部分を抽出し、prefix/suffixを分離する
*/
function parseValue(value: string | number): {
numericValue: number;
prefix: string;
suffix: string;
} {
if (typeof value === "number") {
return { numericValue: value, prefix: "", suffix: "" };
}
const numericMatch = value.match(/[\d,]+/);
if (!numericMatch) {
return { numericValue: 0, prefix: value, suffix: "" };
}
const numericValue = parseFloat(numericMatch[0].replace(/,/g, ""));
const prefixMatch = value.match(/^[^\d]*/);
const prefix = prefixMatch ? prefixMatch[0] : "";
const numericIndex = numericMatch.index ?? 0;
const numericEndIndex = numericIndex + numericMatch[0].length;
const suffix = value.substring(numericEndIndex);
return { numericValue, prefix, suffix };
}
export function KPICard({
title,
value,
description,
metric,
isSelected,
onClick,
}: KPICardProps) {
const { numericValue, prefix, suffix } = parseValue(value);
return (
<Card
className={cn(
"transition-all cursor-pointer hover:shadow-md",
isSelected && "ring-2 ring-blue-600 shadow-md"
)}
onClick={onClick}
>
<CardContent className="p-6">
<p className="text-sm font-medium text-slate-600">{title}</p>
<p className="mt-2 text-3xl font-bold text-slate-900">
{prefix}
<CountUp
start={0}
end={numericValue}
duration={2.5}
separator=","
useEasing={true}
/>
{suffix}
</p>
{description && (
<p className="mt-1 text-xs text-slate-500">{description}</p>
)}
</CardContent>
</Card>
);
}
react-countupを導入し、わずか数行のコードを追加するだけで、ダッシュボードの体験が大きく変わりました。
Before(静止した数値):
After(カウントアップアニメーション):
今回の実装で、ダッシュボードに「ライブ感」を加えることができました。今後は以下のような拡張も検討しています:
個人開発のダッシュボードは、数字の変化を肌で感じることで、開発のモチベーションが維持されます。ぜひ、あなたのプロジェクトにも取り入れてみてください!
次の記事
ありません