
Learn how to implement a custom "Animated Counter" module in HubSpot to visually highlight your company's achievements and KPIs. This guide provides the complete HTML, CSS, JavaScript, and field configuration, allowing you to create a module that is intuitive even for non-developers.


This module visually highlights key company achievements, metrics, or important statistics (e.g., customer satisfaction rates, projects completed) with animated numbers, effectively capturing visitor attention.
display: flex) to align the counter boxes horizontally. The flex-wrap: wrap property is included to ensure the boxes stack vertically on smaller screens, maintaining a responsive design.The following are the fields that content editors will use in the page editor interface.

text_color)Text and Number Colorcolorcounter_boxes)Counter Boxesgroup (with repeater enabled)title)Titletextnumber)Numbernumbersuffix)Suffix (e.g., %)text
<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);
});
});No, it is not. This is a third-party Google Apps Script (GAS) library (an externally developed program). To use it, you must open the Script Editor, add the library using the specified "Script ID" as per the instructions in Step 1, and set up an execution function (e.g., parseMyAddresses) yourself, like the code example in Step 2.
No, you cannot. As stated in the "2. Supported Countries" section of this article, this library currently only supports the address formats of the five specified countries. If you input an address from a country not on the list, it may not be parsed correctly or could cause an error.
The parsed results are not output together in column C. As explained in the execution example (After execution), the parsed components (Country, City, Postal Code, etc.) are output divided into multiple columns—column C, column D, column E, and so on. Also, if the input is in column A and the output starts in column C, the data in the intermediate column B will not be changed and will remain as is.
We can customize this sample to match your specific business requirements.
Book Free Consultation