ReactでアニメーションするスパークラインKPIカードの作り方|SVGで線を描き、塗りと終点のpingまで

公開日: 2026年11月11日 · 読了目安 10 分

ReactでアニメーションするスパークラインKPIカードの作り方です。SVGのpathLengthとstroke-dashoffsetで線を描き、塗りのフェードと終点のpingのタイミング、動きを減らす設定まで、完成カードの試し方とあわせて解説します。

静止したKPIの数字は見落とされやすい

白いKPIカード。スパークラインが左から右へ描かれ、塗りがフェードインし、終点がpingする。
白いKPIカード。スパークラインが左から右へ描かれ、塗りがフェードインし、終点がpingする。

数字はある。ラベルもはっきりしている。差分のピルも緑だ。それでもダッシュボードや分析機能のグリッドでは、そのままスクロールされてしまうことがあります。

問題は、指標が間違っていることではありません。置いてあるだけの数字に見えてしまうことです。トレンドを示す線がなく、「今、目に入った指標だ」と伝える動きもありません。

スパークラインは、その前半を解決します。大きな数字の下に短い線を置けば、直近の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 のダッシュでも考え方は同じです。スケールは一つにそろえてください。

全体で使っているイージングは、強めのイーズアウトです。

ts
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です。

ts
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 まで動かすと最初の点から最後の点まで描かれます。

tsx
<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 かけて出てきます。線が終わるころに、塗りも完了します。

tsx
<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 へ、同じイージングです。遅延があるので、線が描き終わり、終点が見えてから脈打ちが始まります。

css
@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 にし、チャートが空のまま残らないようにします。

tsx
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 で動きを減らす設定がオンなら、線を描画せず、終点も脈打たせません。完成したチャートを、一度で出します。

css
@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 のアニメーションスパークラインを使う

DesignLayer の「スパークライン KPI カード」の詳細ページ。左でライブプレビューを確かめる。ソース全文のコピーは 30日ビルドパスが必要。
DesignLayer の「スパークライン KPI カード」の詳細ページ。左でライブプレビューを確かめる。ソース全文のコピーは 30日ビルドパスが必要。

パスの形、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 モーションコンポーネント で解説しています。

関連ガイド