ReactでアニメーションするスパークラインKPIカードの作り方|SVGで線を描き、塗りと終点のpingまで
公開日: 2026年11月11日 · 読了目安 10 分
ReactでアニメーションするスパークラインKPIカードの作り方です。SVGのpathLengthとstroke-dashoffsetで線を描き、塗りのフェードと終点のpingのタイミング、動きを減らす設定まで、完成カードの試し方とあわせて解説します。
静止したKPIの数字は見落とされやすい

数字はある。ラベルもはっきりしている。差分のピルも緑だ。それでもダッシュボードや分析機能のグリッドでは、そのままスクロールされてしまうことがあります。
問題は、指標が間違っていることではありません。置いてあるだけの数字に見えてしまうことです。トレンドを示す線がなく、「今、目に入った指標だ」と伝える動きもありません。
スパークラインは、その前半を解決します。大きな数字の下に短い線を置けば、直近の12点が上がったのか、下がったのか、揺れたのかが、本格的なチャートなしで分かります。React のアニメーション付きスパークラインは、後半を解決します。カードが画面に入ると線が自分で描かれ、その下の塗りがフェードインし、終点が小さな ping で着地します。主役は数字のままです。動きは、話を終わらせるためだけにあります。
先に短い答えを書きます。React でアニメーションするスパークラインは、pathLength と stroke-dasharray、stroke-dashoffset で自分自身を描く SVG のパスです。多くの場合、カードが画面に入ったタイミングで始めます。少し遅れて塗りをフェードインし、終点と任意の ping を出して、動きが途中で切れたように見せないようにします。OS で動きを減らす設定がオンなら、トランジションをやめて完成した線だけを出します。
この記事で分かることは次のとおりです。
点の配列から、滑らかな SVG パスを作る方法
stroke-dashoffsetで線を描く方法(getTotalLength()もチャートライブラリも使わない)塗りのフェード、終点、ping のタイミングの取り方
IntersectionObserverで動きを始める方法prefers-reduced-motionを尊重し、チャートを読みやすく保つ方法
以下の時間とイージングは、DesignLayer の「スパークライン KPI カード」の実装から取っています。掲載しているコードは短い説明用の抜粋で、ソース全文ではありません。
自作スパークラインが安く見えがちな理由
失敗しやすいのは計算ではなく、動きの最初の1秒です。
最初の描画ですでに線がある
コンポーネントがマウントされた時点でパスが見えていて、CSS アニメーションが1フレーム遅れて始まると、完成した線がもう一度「再生」されて見えます。描画のトリックは、描き始めるまで線を隠しておくときにだけ効きます。stroke-dashoffset をパスの全長にしておくか、カードを画面の外に置いておき、フラグを一つ変えます。
塗りと線が同時に始まる
ストロークと同時に塗りが出ると、「描いている」のではなくフェードに見えます。塗りは、線の下に乗るものが出てからで十分です。数百ミリ秒の遅延があれば足ります。
12点の線にチャートライブラリは大きい
Recharts や Chart.js、Nivo は、ツールチップや軸、範囲の選択には向いています。KPI タイルの十数点には過剰です。300×76 の SVG と CSS トランジションの方が、線はすっきり描けます。イージングと遅延も自分で決められます。Next.js での Motion 全般は Motion のガイド で解説しています。このカード自体は Motion を使いません。
チャートライブラリではなく SVG の pathLength で動かす
方針は、小さく保つことです。
カードは "use client" を付けた React コンポーネントにします。持つ状態は drawn のようなフラグ一つです。点からパスを一度作り、SVG に塗りと線の2本のパスを置きます。drawn が true になったら、両方を CSS トランジションで動かします。フラグのオンは IntersectionObserver で行い、画面外で描画が終わってしまわないようにします。
大事な SVG の指定が pathLength です。ブラウザに「このパスの長さは、あなたが決めた値だ」と伝えるので、カードの幅が変わってもダッシュの単位が安定します。getTotalLength() は呼びません。DesignLayer のカードは pathLength={100} と、100 100 のダッシュパターンを使っています。pathLength={1} と長さ 1 のダッシュでも考え方は同じです。スケールは一つにそろえてください。
全体で使っているイージングは、強めのイーズアウトです。
const EASE = "cubic-bezier(0.22, 1, 0.36, 1)";線は最初に速く進み、最後で落ち着きます。12点のスパークラインでは、動きが終わる前にトレンドが見えている方がよい、という値です。
React でアニメーションするスパークラインKPIカードの作り方
カードの見た目は、シンプルで構いません。ラベル、大きな数字、差分のピル、その下のスパークラインです。DesignLayer のタイルは幅 340px、ラベル 13px、数字 34px、チャートの高さ 76px です。これは出発点であり、必須の寸法ではありません。大切なのは、動きの積み重ねです。
Step 1:点の配列から滑らかな SVG パスを作る
各値を viewBox に載せます。このカードでは幅 300、高さ 76、上下 8px、右 16px の余白です。右の余白は飾りではありません。終点は 10px の点に白いリングが付き、カードは overflow を隠します。隙間がないと、最後の点が切れます。
点は古い順です。2点以上必要です。最小値と最大値で正規化し、変化の少ない系列でも高さを使い切ります(全部同じ値なら、差は 1 とみなします)。
短い直線のつなぎは、小さなチャートでは硬く見えます。Catmull-Rom スプラインを三次ベジェに変換すれば足ります。隣り合う点の制御点は、前後の点の差の6分の1です。
const W = 300;
const H = 76;
const PAD_Y = 8;
const PAD_R = 16; // 終点のドットが切れないための余白
function point(values: readonly number[], i: number) {
const min = Math.min(...values);
const max = Math.max(...values);
const span = max - min || 1;
return {
x: (i / (values.length - 1)) * (W - PAD_R),
y: H - PAD_Y - ((values[i]! - min) / span) * (H - PAD_Y * 2),
};
}
// 点 p0, p1, p2, p3(無い隣点は端の点を再利用):
// c1 = p1 + (p2 - p0) / 6
// c2 = p2 - (p3 - p1) / 6
// そして: C c1.x c1.y, c2.x c2.y, p2.x p2.y塗りのパスは、線と同じ d のあと、最後の x のまま viewBox の下辺へ折り、0, H に戻して Z で閉じます。塗りには、上から透明へ落ちる縦方向のグラデーションを付けます。DesignLayer のカードでは、線の色を上で 22% の不透明度、下で 0 にしています。
カード幅いっぱいに伸ばすなら preserveAspectRatio="none" を使います。ストロークの端が欠けないよう、SVG には overflow-visible を付けます。角をきれいに切りたいなら、カード側の overflow-hidden はそのままにします。
Step 2:stroke-dashoffset で線を描く
線のパスに pathLength={100} を付けます。ダッシュ配列 100 100 は、パスと同じ長さの実線と、同じ長さの隙間です。オフセットを 100 にすると線は隠れ、0 まで動かすと最初の点から最後の点まで描かれます。
<path
d={line}
pathLength={100}
fill="none"
stroke={color}
strokeWidth={2.25}
strokeLinecap="round"
strokeLinejoin="round"
style={{
strokeDasharray: "100 100",
strokeDashoffset: drawn ? 0 : 100,
transition: `stroke-dashoffset 1400ms ${EASE}`,
}}
/>1400ms は、トレンドを追える長さであり、ダッシュボードの1行に並べても遅く感じにくい長さです。pathLength が固定なので、大きな画面でカードが広くなっても、描画時間は変わりません。
drawn は React の state で切り替えます。カードが画面の下にあるかもしれないなら、マウントと同時にトランジションを始めないでください。それは Step 4 です。
Step 3:塗りをフェードインし、終点の ping を着地させる
塗りは、2本目のダッシュアニメーションではなく、遅延付きの opacity トランジションです。drawn が true になってから 500ms 後に始まり、900ms かけて出てきます。線が終わるころに、塗りも完了します。
<path
d={area}
fill={`url(#${gradientId})`}
style={{
opacity: drawn ? 1 : 0,
transition: `opacity 900ms ${EASE} 500ms`,
}}
/>終点は SVG の円ではなく、絶対配置の小さなドットです。ping のリングを CSS で作りやすいからです。位置は最後の点を使います。left は (end.x / W) * 100%、top は SVG の下に置いた高さ 0 のラッパーから -(H - end.y) です。最初は scale(0.4) で opacity 0 にします。drawn が true なら、1250ms 遅れて opacity を 300ms、スケールを 1 まで 420ms で動かします。線が終わる直前です。
ping はそのドットの子です。1.8秒のアニメーションを 1.6秒遅らせて、繰り返し再生します。スケールは 1 から 3、不透明度は 0.5 から 0 へ、同じイージングです。遅延があるので、線が描き終わり、終点が見えてから脈打ちが始まります。
@keyframes sparkline-kpi-card-ping {
0% { transform: scale(1); opacity: 0.5; }
100% { transform: scale(3); opacity: 0; }
}
.sparkline-kpi-card-ping {
animation: sparkline-kpi-card-ping 1.8s cubic-bezier(0.22, 1, 0.36, 1) 1.6s infinite;
}実装上の注意です。アニメーション用のクラスをマウント時点から付けていると、1.6秒の遅延はその時点から始まり、drawn が変わった時点からは始まりません。カードが最初から画面内なら問題になりません。ページの下の方にあるなら、drawn が true になってからクラスを付け、遅延が描画と揃うようにします。
色は差分に合わせます。DesignLayer のカードでは、上昇が #10b981、下降が #f43f5e で、塗り、線、ドット、ping まで同じ色です。
Step 4:カードが画面に入ったときに動きを始める
IntersectionObserver の threshold は 0.35 で足ります。カードの 35% が見えたら drawn を true にして、observer を切断します。入場の動きは一度だけで十分です。IntersectionObserver が無い環境では、すぐに drawn を true にし、チャートが空のまま残らないようにします。
React.useEffect(() => {
const node = ref.current;
if (!node || typeof IntersectionObserver === "undefined") {
setDrawn(true);
return;
}
const observer = new IntersectionObserver(
([entry]) => {
if (entry?.isIntersecting) {
setDrawn(true);
observer.disconnect();
}
},
{ threshold: 0.35 },
);
observer.observe(node);
return () => observer.disconnect();
}, []);スクロールで要素を見せるときと同じ考え方ですが、切り替えているのは、すでに書いた CSS トランジションだけです。アニメーションライブラリも、スクロールのリスナーも要りません。
Step 5:動きを減らす設定を尊重し、チャートを読みやすく保つ
OS で動きを減らす設定がオンなら、線を描画せず、終点も脈打たせません。完成したチャートを、一度で出します。
@media (prefers-reduced-motion: reduce) {
.sparkline-kpi-card-ping {
animation: none;
opacity: 0;
}
}パスと終点には、Tailwind の motion-reduce:!transition-none(または同じ内容の CSS)を付け、遅延を無効にします。drawn が true になると、オフセットはすぐに 0 になり、塗りも一気に現れます。ping は出ません。
SVG は飾りではなく、画像として扱います。DesignLayer のカードは role="img" と、${label} trend, ${values.length} points の形の aria-label を付けています。数字には tabular-nums を使い、値が変わっても $48,210 のような桁が揺れないようにしています。終点の ping は aria-hidden のままで構いません。トレンドの説明は、SVG 側のラベルが担います。
完成した React のアニメーションスパークラインを使う

パスの形、3つの遅延、ping を一つの動きとして揃えるには、抜粋を見るより時間がかかります。組み直す前に完成形を見たいときは、DesignLayer の スパークライン KPI カード を開いてください。ライブプレビューは、この記事で説明した動きそのものです。白いタイル、上昇する系列、緑の差分、左から右へ描かれる線、フェードインする塗り、終点の ping です。
閲覧とライブプレビューは無料です。見るだけなら、アカウントもカードも要りません。このコンポーネントは無料サンプルではありません。ソース全文のコピーは、30日ビルドパスまたは Lifetime Access に含まれます(月額の Plus もあります)。
「Cursor 用にコピー」を押すと、組み込みの指示、ライブプレビューの URL、本体の TSX、npm の一覧がまとめてコピーされます。Cursor や Claude に貼り、ダッシュボードの1行や分析機能のグリッドに置けます。ラベル、整形済みの数値、差分、点の配列は差し替えてください。
無料アカウントでも、無料サンプル 10 個は回数の上限なくコピーでき、ローンチ動画のプロンプトは月1回です。カタログ全体(146 個、うち 136 個は Pass 限定)の回数無制限のコピーは、30日ビルドパス($19 の1回払い)、Lifetime Access($79 の1回払い、先着100名のファウンディング価格)、または Plus(月額 $9、いつでも解約可)です。購入前に、現行の案内を 料金ページ で確認してください。カタログは https://design-layer.com/ja/components です。
よくある質問
できます。SVG のパスを描き、
stroke-dashoffsetをパスの長さから 0 まで動かします。pathLengthでダッシュの単位を安定させ、IntersectionObserverでdrawnフラグを切り替え、CSS トランジションで動かします。Recharts や Chart.js、getTotalLength()は不要です。
まとめ:React のアニメーション付きスパークラインKPIカードは5つの手順
点から滑らかな SVG パスを作り、右に終点用の余白を残す
pathLengthとstroke-dashoffsetで線を描く。目安は 1400ms、イーズアウト少し遅れて塗りをフェードインし、終点と ping を出す
IntersectionObserverでdrawnフラグを切り替える(threshold 0.35 が扱いやすい)動きを減らす設定では、完成したチャートだけを出し、脈打ちは止める
まずは Step 2 と Step 4 を、タイル1つに付けてみてください。タイミングが決まったら、塗りと ping を足します。完成カードから試したい場合は スパークライン KPI カード を開いてください。Motion を使った UI(このカードは使いません)は Framer Motion のガイド と マグネティックボタンのガイド で、Cursor へのコピーの流れは Cursor 向け React モーションコンポーネント で解説しています。



