数字カウントアップ

要素が画面に入った瞬間、実績数値を0からアニメーションで加算表示します。サービス紹介の統計に。

#javascript#intersection-observer#animation

ライブデモ

使用例(お題: SaaS FlowDesk)

この技法を「SaaS FlowDesk」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- FlowDesk 実績セクション。枠に入った瞬間に数値がカウントアップ -->
<div class="fdc-scroller" id="cuScroller">
  <header class="fdc-hero">
    <span class="fdc-badge">TRUSTED BY TEAMS</span>
    <h1>数字で見る FlowDesk</h1>
    <p>導入企業とともに積み上げてきた、いまのわたしたち。</p>
    <span class="fdc-arrow" aria-hidden="true">&#8595;</span>
  </header>

  <section class="fdc-stats">
    <div class="fdc-stat">
      <span class="fdc-num" data-target="12800" data-suffix="社">0</span>
      <span class="fdc-cap">導入企業数</span>
    </div>
    <div class="fdc-stat">
      <span class="fdc-num" data-target="340" data-suffix="万人">0</span>
      <span class="fdc-cap">アクティブユーザー</span>
    </div>
    <div class="fdc-stat">
      <span class="fdc-num" data-target="99" data-suffix=".9%">0</span>
      <span class="fdc-cap">稼働率(SLA)</span>
    </div>
    <div class="fdc-stat">
      <span class="fdc-num" data-target="42" data-suffix="%">0</span>
      <span class="fdc-cap">作業時間の削減</span>
    </div>
  </section>

  <footer class="fdc-cta">
    <h2>あなたのチームでも。</h2>
    <button class="fdc-btn" type="button">14日間 無料で始める</button>
    <p class="fdc-foot">FlowDesk — 仕事の流れを、ひとつに。</p>
  </footer>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --navy: #0f1b34;
  --blue: #4f7cff;
  --white: #ffffff;
}

body {
  font-family: "Hiragino Kaku Gothic ProN", "Segoe UI", sans-serif;
  background: var(--navy);
  color: var(--white);
  -webkit-font-smoothing: antialiased;
}

/* 内部スクロール領域 */
.fdc-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
  scrollbar-color: var(--blue) transparent;
}

/* ヒーロー */
.fdc-hero {
  height: 220px;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  text-align: center;
  padding: 0 20px;
  background:
    radial-gradient(circle at 25% 20%, rgba(79,124,255,.35), transparent 60%),
    radial-gradient(circle at 80% 90%, rgba(79,124,255,.2), transparent 55%),
    var(--navy);
}
.fdc-badge {
  font-size: .6rem;
  letter-spacing: .26em;
  padding: 5px 13px;
  border-radius: 999px;
  background: rgba(79,124,255,.18);
  color: #aac0ff;
}
.fdc-hero h1 { font-size: 1.9rem; font-weight: 800; }
.fdc-hero p { font-size: .82rem; color: #aeb9da; }
.fdc-arrow { margin-top: 4px; font-size: 1.4rem; color: var(--blue); animation: fdcBob 1.6s ease-in-out infinite; }
@keyframes fdcBob {
  0%,100% { transform: translateY(0); opacity: .6; }
  50% { transform: translateY(8px); opacity: 1; }
}

/* 統計グリッド */
.fdc-stats {
  max-width: 540px;
  margin: 0 auto;
  padding: 40px 24px;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 16px;
}
.fdc-stat {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 26px 14px;
  border-radius: 16px;
  background: rgba(255,255,255,.04);
  border: 1px solid rgba(79,124,255,.18);
}
.fdc-num {
  font-size: 2rem;
  font-weight: 800;
  letter-spacing: .01em;
  background: linear-gradient(120deg, #8fb0ff, var(--blue));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  font-variant-numeric: tabular-nums;
}
.fdc-cap { font-size: .76rem; color: #aeb9da; letter-spacing: .04em; }

/* CTA */
.fdc-cta {
  text-align: center;
  padding: 24px 24px 80px;
}
.fdc-cta h2 { font-size: 1.3rem; font-weight: 800; margin-bottom: 16px; }
.fdc-btn {
  font: inherit;
  font-weight: 700;
  font-size: .92rem;
  color: #fff;
  padding: 13px 30px;
  border: none;
  border-radius: 999px;
  background: linear-gradient(120deg, #6a93ff, var(--blue));
  box-shadow: 0 16px 30px -14px rgba(79,124,255,.9);
  cursor: pointer;
  transition: transform .2s ease;
}
.fdc-btn:hover { transform: translateY(-2px); }
.fdc-foot { font-size: .72rem; color: #8595c0; letter-spacing: .08em; margin-top: 18px; }

@media (prefers-reduced-motion: reduce) {
  .fdc-arrow { animation: none; }
  .fdc-btn { transition: none; }
}
JavaScript
// FlowDesk 実績:枠に入った瞬間に数値を0からカウントアップ
(() => {
  const scroller = document.getElementById('cuScroller');
  const nums = Array.from(document.querySelectorAll('.fdc-num'));
  if (!nums.length) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // 1つの数字をアニメーションで増やす
  function animateNum(el) {
    const target = Number(el.dataset.target) || 0;
    const prefix = el.dataset.prefix || '';
    const suffix = el.dataset.suffix || '';
    const duration = 1600;

    if (reduce) {
      el.textContent = prefix + target.toLocaleString('ja-JP') + suffix;
      return;
    }

    const start = performance.now();
    function tick(now) {
      const t = Math.min((now - start) / duration, 1);
      // easeOutCubic で減速
      const eased = 1 - Math.pow(1 - t, 3);
      const val = Math.round(target * eased);
      el.textContent = prefix + val.toLocaleString('ja-JP') + suffix;
      if (t < 1) requestAnimationFrame(tick);
    }
    requestAnimationFrame(tick);
  }

  if (scroller && 'IntersectionObserver' in window) {
    const io = new IntersectionObserver((entries, obs) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          animateNum(entry.target);
          obs.unobserve(entry.target); // 一度だけ
        }
      });
    }, { root: scroller, threshold: 0.5 });
    nums.forEach(n => io.observe(n));
  } else {
    nums.forEach(animateNum);
  }

  // 操作がなくてもカウントが見えるよう、ゆっくり自動スクロール
  if (scroller && !reduce) {
    let auto = true;
    const stop = () => { auto = false; };
    ['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
      scroller.addEventListener(ev, stop, { passive: true }));

    setTimeout(function step() {
      if (!auto) return;
      const max = scroller.scrollHeight - scroller.clientHeight;
      if (scroller.scrollTop >= max - 1) return;
      scroller.scrollTop += 3;
      requestAnimationFrame(step);
    }, 800);
  }
})();

コード

HTML
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="cu-scroller" id="cuScroller">
  <div class="cu-spacer">
    <span>&#8595; スクロールで数字が動く</span>
  </div>

  <!-- 枠に入ったらカウントアップするスタッツ -->
  <section class="cu-stats" id="cuStats">
    <h2 class="cu-title">私たちの実績</h2>
    <div class="cu-grid">
      <div class="cu-item">
        <span class="cu-num" data-target="128000" data-suffix="+">0</span>
        <span class="cu-label">ダウンロード</span>
      </div>
      <div class="cu-item">
        <span class="cu-num" data-target="98" data-suffix="%">0</span>
        <span class="cu-label">満足度</span>
      </div>
      <div class="cu-item">
        <span class="cu-num" data-target="42" data-prefix="">0</span>
        <span class="cu-label">対応言語</span>
      </div>
      <div class="cu-item">
        <span class="cu-num" data-target="1500" data-suffix="万円">0</span>
        <span class="cu-label">調達額</span>
      </div>
    </div>
  </section>
</div>
CSS
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --accent: #18d6c4;
  --accent2: #5b8cff;
}

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background: #0d1117;
  color: #eef1f7;
}

/* プレビュー枠を埋める自前スクロール領域 */
.cu-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
}

.cu-spacer {
  height: 280px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #8b93a7;
  font-size: .9rem;
  letter-spacing: .06em;
  background:
    radial-gradient(circle at 50% 120%, rgba(91,140,255,.18), transparent 60%);
}

.cu-stats {
  padding: 56px 24px 90px;
  text-align: center;
}
.cu-title {
  font-size: 1.6rem;
  font-weight: 800;
  margin-bottom: 38px;
  background: linear-gradient(90deg, var(--accent), var(--accent2));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.cu-grid {
  max-width: 640px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 18px;
}
.cu-item {
  padding: 28px 16px;
  border-radius: 16px;
  background: rgba(255,255,255,.04);
  border: 1px solid rgba(255,255,255,.08);
}
.cu-num {
  display: block;
  font-size: 2.3rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums; /* 桁ブレ防止 */
  background: linear-gradient(90deg, var(--accent), var(--accent2));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  line-height: 1.1;
}
.cu-label {
  display: block;
  margin-top: 8px;
  font-size: .85rem;
  color: #9aa2b6;
  letter-spacing: .04em;
}

@media (max-width: 480px) {
  .cu-grid { grid-template-columns: 1fr; }
}
JavaScript
// 自前スクロール領域内で、枠に入った瞬間にカウントアップ
(() => {
  const scroller = document.getElementById('cuScroller');
  const nums = Array.from(document.querySelectorAll('.cu-num'));
  if (!nums.length) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // 1つの数字をアニメーションで増やす
  function animateNum(el) {
    const target = Number(el.dataset.target) || 0;
    const prefix = el.dataset.prefix || '';
    const suffix = el.dataset.suffix || '';
    const duration = 1600;

    if (reduce) {
      el.textContent = prefix + target.toLocaleString('ja-JP') + suffix;
      return;
    }

    const start = performance.now();
    function tick(now) {
      const t = Math.min((now - start) / duration, 1);
      // easeOutCubic で減速
      const eased = 1 - Math.pow(1 - t, 3);
      const val = Math.round(target * eased);
      el.textContent = prefix + val.toLocaleString('ja-JP') + suffix;
      if (t < 1) requestAnimationFrame(tick);
    }
    requestAnimationFrame(tick);
  }

  if (scroller && 'IntersectionObserver' in window) {
    const io = new IntersectionObserver((entries, obs) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          animateNum(entry.target);
          obs.unobserve(entry.target); // 一度だけ
        }
      });
    }, { root: scroller, threshold: 0.5 });
    nums.forEach(n => io.observe(n));
  } else {
    nums.forEach(animateNum);
  }

  // 操作がなくてもカウントが見えるよう、ゆっくり自動スクロール
  if (scroller && !reduce) {
    let auto = true;
    const stop = () => { auto = false; };
    ['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
      scroller.addEventListener(ev, stop, { passive: true }));

    setTimeout(function step() {
      if (!auto) return;
      const max = scroller.scrollHeight - scroller.clientHeight;
      if (scroller.scrollTop >= max - 1) return;
      scroller.scrollTop += 3;
      requestAnimationFrame(step);
    }, 800);
  }
})();

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「数字カウントアップ」の効果を追加してください。

# 追加してほしい効果
数字カウントアップ(スクロール演出)
要素が画面に入った瞬間、実績数値を0からアニメーションで加算表示します。サービス紹介の統計に。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- 自前のスクロール領域(プレビュー枠内で完結) -->
<div class="cu-scroller" id="cuScroller">
  <div class="cu-spacer">
    <span>&#8595; スクロールで数字が動く</span>
  </div>

  <!-- 枠に入ったらカウントアップするスタッツ -->
  <section class="cu-stats" id="cuStats">
    <h2 class="cu-title">私たちの実績</h2>
    <div class="cu-grid">
      <div class="cu-item">
        <span class="cu-num" data-target="128000" data-suffix="+">0</span>
        <span class="cu-label">ダウンロード</span>
      </div>
      <div class="cu-item">
        <span class="cu-num" data-target="98" data-suffix="%">0</span>
        <span class="cu-label">満足度</span>
      </div>
      <div class="cu-item">
        <span class="cu-num" data-target="42" data-prefix="">0</span>
        <span class="cu-label">対応言語</span>
      </div>
      <div class="cu-item">
        <span class="cu-num" data-target="1500" data-suffix="万円">0</span>
        <span class="cu-label">調達額</span>
      </div>
    </div>
  </section>
</div>

【CSS】
* { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --accent: #18d6c4;
  --accent2: #5b8cff;
}

body {
  font-family: "Segoe UI", system-ui, sans-serif;
  background: #0d1117;
  color: #eef1f7;
}

/* プレビュー枠を埋める自前スクロール領域 */
.cu-scroller {
  width: 100%;
  height: 100vh;
  max-height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: thin;
}

.cu-spacer {
  height: 280px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #8b93a7;
  font-size: .9rem;
  letter-spacing: .06em;
  background:
    radial-gradient(circle at 50% 120%, rgba(91,140,255,.18), transparent 60%);
}

.cu-stats {
  padding: 56px 24px 90px;
  text-align: center;
}
.cu-title {
  font-size: 1.6rem;
  font-weight: 800;
  margin-bottom: 38px;
  background: linear-gradient(90deg, var(--accent), var(--accent2));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.cu-grid {
  max-width: 640px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 18px;
}
.cu-item {
  padding: 28px 16px;
  border-radius: 16px;
  background: rgba(255,255,255,.04);
  border: 1px solid rgba(255,255,255,.08);
}
.cu-num {
  display: block;
  font-size: 2.3rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums; /* 桁ブレ防止 */
  background: linear-gradient(90deg, var(--accent), var(--accent2));
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  line-height: 1.1;
}
.cu-label {
  display: block;
  margin-top: 8px;
  font-size: .85rem;
  color: #9aa2b6;
  letter-spacing: .04em;
}

@media (max-width: 480px) {
  .cu-grid { grid-template-columns: 1fr; }
}

【JavaScript】
// 自前スクロール領域内で、枠に入った瞬間にカウントアップ
(() => {
  const scroller = document.getElementById('cuScroller');
  const nums = Array.from(document.querySelectorAll('.cu-num'));
  if (!nums.length) return; // null安全

  const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

  // 1つの数字をアニメーションで増やす
  function animateNum(el) {
    const target = Number(el.dataset.target) || 0;
    const prefix = el.dataset.prefix || '';
    const suffix = el.dataset.suffix || '';
    const duration = 1600;

    if (reduce) {
      el.textContent = prefix + target.toLocaleString('ja-JP') + suffix;
      return;
    }

    const start = performance.now();
    function tick(now) {
      const t = Math.min((now - start) / duration, 1);
      // easeOutCubic で減速
      const eased = 1 - Math.pow(1 - t, 3);
      const val = Math.round(target * eased);
      el.textContent = prefix + val.toLocaleString('ja-JP') + suffix;
      if (t < 1) requestAnimationFrame(tick);
    }
    requestAnimationFrame(tick);
  }

  if (scroller && 'IntersectionObserver' in window) {
    const io = new IntersectionObserver((entries, obs) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          animateNum(entry.target);
          obs.unobserve(entry.target); // 一度だけ
        }
      });
    }, { root: scroller, threshold: 0.5 });
    nums.forEach(n => io.observe(n));
  } else {
    nums.forEach(animateNum);
  }

  // 操作がなくてもカウントが見えるよう、ゆっくり自動スクロール
  if (scroller && !reduce) {
    let auto = true;
    const stop = () => { auto = false; };
    ['wheel', 'touchstart', 'pointerdown'].forEach(ev =>
      scroller.addEventListener(ev, stop, { passive: true }));

    setTimeout(function step() {
      if (!auto) return;
      const max = scroller.scrollHeight - scroller.clientHeight;
      if (scroller.scrollTop >= max - 1) return;
      scroller.scrollTop += 3;
      requestAnimationFrame(step);
    }, 800);
  }
})();

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。