HubSpotモジュール:HubSpotにおける主要指標表示用の再利用可能な『アニメーションカウンター』モジュール構築手順

HubSpotでカスタム「アニメーション付きカウンター」モジュールを実装し、会社の実績やKPIを視覚的に際立たせる方法を学びましょう。

このガイドでは、完全な HTML、CSS、JavaScript、フィールド設定を提供しており、開発者でない人でも直感的に利用できるモジュールを作成できるようになります。

Demo Video

Detail

カウンターモジュール

このモジュールは、会社の主要な実績、指標、重要な統計(例: 顧客満足度、完了したプロジェクト数)をアニメーション付きの数値で視覚的に強調し、訪問者の注意を効果的に引きつけます。

できること

  • 編集者は必要に応じて、カウンターボックスを自由に追加・削除・並べ替え可能。
  • 各ボックスは「タイトル」「数値」「サフィックス(例: % や 件)」で構成。
  • ページをスクロールしてモジュールが表示されると、数値が 0 から指定された目標値までカウントアップするアニメーションが動作。
  • モジュール内のすべてのテキストと数値の色は、1つのカラーピッカーで一括変更可能。

実装ポイント & 注意事項

  • フィールド設定(リピーター):
    このモジュールは リピーターオプションを有効にしたグループフィールドを使用。これにより、コンテンツ編集者は必要な数だけ動的にカウンターを追加可能。
  • アニメーショントリガー:
    JavaScript の Intersection Observer API を使用。モジュールがブラウザのビューポートに入ったときにカウントアニメーションを開始するため、パフォーマンス最適化にも有効。
  • レスポンシブレイアウト:
    CSS Flexbox (display: flex) を使用してカウンターボックスを横並びに配置。flex-wrap: wrap プロパティにより、小さい画面では縦積みになり、レスポンシブデザインを維持。

詳細な入力フィールド

  • テキストと数値の色 (text_color)
    • ラベル: Text and Number Color
    • タイプ: color
    • 説明: このモジュール内のすべてのテキスト要素(タイトル、数値、サフィックス)の色をカラーピッカーで一括設定。
  • カウンターボックス (counter_boxes)
    • ラベル: Counter Boxes
    • タイプ: group(リピーター有効)
    • 説明: 複数のカウンターボックスを作成可能な繰り返しフィールド。「+Add」ボタンで必要な数だけ生成できる。
    • 子フィールド:
      • タイトル (title)
        • ラベル: Title
        • タイプ: text
        • 説明: 数値が何を表しているかを説明する見出し(例: 「成約率」「顧客満足度」)。
      • 数値 (number)
        • ラベル: Number
        • タイプ: number
        • 説明: カウンターがアニメーションで到達する最終的な目標値。
      • サフィックス (suffix)
        • ラベル: Suffix(例: %)
        • タイプ: text
        • 説明: 数値の後に付ける単位や記号(例: 「%」「百万」「人」)。

Source Code

HTML
<div class="counter-wrapper">
  {% for box in module.counter_boxes %}
    <div class="counter-box">
      <h2 class="counter-title" style="color: {{ module.text_color.color }};">
        {{ box.title }}
      </h2>
      <div class="counter-number-container" style="color: {{ module.text_color.color }};">
        <span class="counter-number" data-target="{{ box.number }}">0</span>
        <span class="counter-suffix">{{ box.suffix }}</span>
      </div>
    </div>
  {% endfor %}
</div>
{# Load the CSS and JavaScript files #}
<link rel="stylesheet" href="{{ module.path }}/module.css">
<script src="{{ module.path }}/module.js" defer></script>

CSS
.logo-slider-wrapper {
  width: 100%;
  overflow: hidden;
  background: #ffffff;
  padding: 40px 0;
  position: relative;
}
.counter-wrapper {
  display: flex;
  justify-content: space-around;
  align-items: center;
  flex-wrap: wrap; /* Wrap items if the screen is narrow */
  text-align: center;
  padding: 20px;
  font-family: sans-serif;
}
.counter-box {
  padding: 20px;
  min-width: 200px;
}
.counter-title {
  font-size: 1.5em;
  margin-bottom: 10px;
}
.counter-number-container {
  font-size: 4em;
  font-weight: bold;
}
.counter-suffix {
  font-size: 0.8em;
  margin-left: 5px;
}
.logo-slider-container {
  position: relative;
  width: 100%;
  max-width: 100%;
}
.logo-slider-track {
  display: flex;
  align-items: center;
  white-space: nowrap;
  animation: logoSlide 25s linear infinite;
  will-change: transform;
}
.logo-slider-item {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 200px;
  height: 100px;
  margin-right: 60px;
  padding: 15px;
}
.logo-slider-item img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  filter: grayscale(100%);
  opacity: 0.7;
  transition: all 0.3s ease;
}
.logo-slider-item:hover img {
  filter: grayscale(0%);
  opacity: 1;
  transform: scale(1.05);
}
/* HubSpot compatibility: Removed 0% and specified only 100% */
@keyframes logoSlide {
  100% {
    transform: translateX(-50%);
  }
}
/* Pause on hover */
.logo-slider-wrapper:hover .logo-slider-track {
  animation-play-state: paused;
}
/* Responsive */
@media (max-width: 768px) {
  .logo-slider-track {
    animation-duration: 30s;
  }

  .logo-slider-item {
    width: 150px;
    height: 75px;
    margin-right: 40px;
    padding: 10px;
  }

  .logo-slider-wrapper {
    padding: 30px 0;
  }
}
@media (max-width: 480px) {
  .logo-slider-track {
    animation-duration: 35s;
  }

  .logo-slider-item {
    width: 120px;
    height: 60px;
    margin-right: 30px;
    padding: 8px;
  }

  .logo-slider-wrapper {
    padding: 20px 0;
  }
}
/* Accessibility support */
@media (prefers-reduced-motion: reduce) {
  .logo-slider-track {
    animation: none;
  }
}

Javascript
document.addEventListener('DOMContentLoaded', () => {
  const counters = document.querySelectorAll('.counter-number');
  const animationDuration = 2000; // Animation duration (in milliseconds)

  const startCounter = (counter) => {
    const target = +counter.getAttribute('data-target');
    let current = 0;

    // Calculate how much to increase the number in each step of the animation
    // The larger the target value, the larger the increment per step
    const increment = target / (animationDuration / 16); // Assuming 60fps

    const updateCounter = () => {
      current += increment;
      if (current < target) {
        counter.innerText = Math.ceil(current).toLocaleString();
        requestAnimationFrame(updateCounter); // Update again on the next frame
      } else {
        counter.innerText = target.toLocaleString(); // Finally, set the target value
      }
    };
    updateCounter();
  };

  const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      // Start the animation when the element enters the viewport
      if (entry.isIntersecting) {
        startCounter(entry.target);
        // Stop observing once it has been executed
        observer.unobserve(entry.target);
      }
    });
  }, {
    threshold: 0.5 // Trigger when 50% of the element is visible
  });

  // Add each counter element to the observer
  counters.forEach(counter => {
    observer.observe(counter);
  });
});