Logo
Home
Resources

Product

Custom Workflow

Resources

Blog
Youtube
Template
Home
>
Product
>
Template Top
>
detail

[Hubspot modules]How to Build a Reusable "Animated Counter" Module in HubSpot to Showcase Key Metrics

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.

Demo Video

Over view
Code

Detail

Counter Module

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.

What You Can Do

  • Editors can freely add, delete, and reorder as many counter boxes as needed. Each box consists of a "Title," a "Number," and a "Suffix" (e.g., "%" or "Cases").
  • When a user scrolls down the page and the module becomes visible, the numbers animate, counting up from 0 to the specified target value.
  • The color of all text and numbers in the module can be changed at once using a single color picker.

Implementation Points & Notes

  • Field Configuration (Repeater):This module utilizes a Group field with the Repeater option enabled. This setup allows content editors to dynamically add as many counters as they require.
  • Animation Trigger:The animation is powered by JavaScript using the Intersection Observer API. This ensures the counting animation only begins when the module enters the browser's viewport, which is an effective performance optimization technique.
  • Responsive Layout:The layout is built with CSS Flexbox (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.

Detailed Input Fields

The following are the fields that content editors will use in the page editor interface.

‍

  1. Text and Number Color (text_color)
    • Label: Text and Number Color
    • Type: color
    • Description: Allows you to globally set the color for all text elements within this module (title, number, and suffix) using a color picker.
  2. Counter Boxes (counter_boxes)
    • Label: Counter Boxes
    • Type: group (with repeater enabled)
    • Description: This is the main repeatable field that allows you to create multiple counter boxes. By clicking the "+Add" button, you can generate as many boxes as needed. Each box contains the following three settings.
    • Child Fields:
      • Title (title)
        • Label: Title
        • Type: text
        • Description: The heading that describes what the number represents (e.g., "Closing Rate," "Customer Satisfaction").
      • Number (number)
        • Label: Number
        • Type: number
        • Description: The final target number you want the counter to animate to.
      • Suffix (suffix)
        • Label: Suffix (e.g., %)
        • Type: text
        • Description: The unit or symbol that appears after the number (e.g., "%," "Million," "People").

‍

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);
  });
});

FAQ

Is this "AddressParserLibrary" a standard feature (a built-in function) of Google Sheets?

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.

Can I parse addresses from countries other than the supported ones (UK, USA, Japan, China, Canada), for example, addresses in Australia or France?

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.

As in the execution example, if the input is in column A and the output starts in column C, will all the parsed results be output together in column C? Also, what happens to column B in between?

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.

Search

Search more

Related Template

Need Customization?

We can customize this sample to match your specific business requirements.

Book Free Consultation

HubSpot Custom Workflow Guide: How to Auto-Search and Recommend Marketing Events

HubSpot Custom Workflow Guide: How to Auto-Recommend Files with Generative AI

HubSpot Custom Workflow Guide: How to "Search" for Marketing Campaigns and Use Their Data

HubSpot Custom Workflow Guide: How to "Search" for Tickets and Auto-Associate Contacts

HubSpot Custom Workflow Guide: How to "Search" for Deals and Auto-Associate Contacts

Company Info
Name : SweetsVillage .Inc
CEO :
‍
Tomoo Motoyama

HomeTemplateCustomWorkflow
Terms & ConditionsPrivacy PolicyContact us

Copyright ©SweetsVillage .Inc

Back To Top Image