HubSpotでカスタム「アニメーション付きカウンター」モジュールを実装し、会社の実績やKPIを視覚的に際立たせる方法を学びましょう。
このガイドでは、完全な HTML、CSS、JavaScript、フィールド設定を提供しており、開発者でない人でも直感的に利用できるモジュールを作成できるようになります。
このモジュールは、会社の主要な実績、指標、重要な統計(例: 顧客満足度、完了したプロジェクト数)をアニメーション付きの数値で視覚的に強調し、訪問者の注意を効果的に引きつけます。
flex-wrap: wrap
プロパティにより、小さい画面では縦積みになり、レスポンシブデザインを維持。
<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>
.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;
}
}
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);
});
});