Instruction

This page provides helpful tips for customizing advanced features.

Counting Animation (GSAP)

This guide explains exactly how to set up your counter animation in Webflow using your custom GSAP code.

What This Animation Does:

The counter animation automatically counts from 0 to the number you place inside the element — even if the number has special characters like “+”, “%”, “K”, etc. Example: 250+ → it will count from 0 to 250+.

The animation starts only when the user scrolls the number into view.

Webflow Setup (Structure)

Step 1 — Add a wrapper

  • Add a div block

  • Give it this class: counter_wrapper

Step 2 — Add the number element

  • Inside the wrapper, add a Text Block

  • Give it this class: counter

  • Type your final number inside the text (example: 450+, 2,500, 90%, etc.)

Webflow Custom Code Placement

Step 1 — Add GSAP + ScrollTrigger

Place your GSAP CDN scripts in:
Project Settings → Custom Code → Head

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>

Step 2 — Add your custom script

Paste your full code inside:
Project Settings → Custom Code → Footer

<script>

  gsap.registerPlugin(ScrollTrigger);

  document.addEventListener("DOMContentLoaded", () => {
    initCounterAnimations();
  });

  /* ========== GSAP COUNTER ========== */
  function initCounterAnimations() {
    const counters = document.querySelectorAll(".counter");

    counters.forEach((counter) => {
      const targetText = counter.textContent.trim();
      const targetNumber = parseFloat(targetText.replace(/[^\d.]/g, "")) || 0;
      const suffix = targetText.replace(/[\d.,\s]/g, "");

      // Start from 0
      gsap.set(counter, { innerText: 0 });

      gsap.to(counter, {
        innerText: targetNumber,
        duration: 3.5,
        ease: "power2.out",
        snap: { innerText: 1 },
        scrollTrigger: {
          trigger: counter,
          start: "top 85%",
          toggleActions: "play none none reset"
        },
        onUpdate: function () {
          const value = Math.ceil(this.targets()[0].innerText);
          counter.innerText = value.toLocaleString("en-US") + suffix;
        }
      });
    });
  }
</script>
  • It looks at the text inside .counter and extracts the number part.

  • It resets the counter to 0 before the animation starts.

  • Using GSAP, it smoothly counts from 0your final number.

  • The animation starts when the user scrolls to 85% of the viewport height (near visible area).

  • You can adjust the animation duration by changing the value of duration.

  • You can adjust the animation easing by changing the value of ease.

  • You can adjust the animation scroll start point by changing the value of start.

Removing Counting Animation

If you want to remove the GSAP Number counting animation from your project, follow these steps:

  • Go to your Site Settings → Custom Code → Footer Code.

  • Find the script below with the comment: GSAP COUNTER

  • Delete the script that appears below this comment. And it's done

  • Or if you want to keep the code but want to delete the animation from an element , just remove the class: counter

Testimonial Tab Switching (GSAP)

This guide explains exactly how to set up and customize your Testimonial Tab Switching in Webflow using your custom GSAP code.

Your Webflow Structure

Tab Structure:

testimonial_tabs (Parent)
├── testimonial-tabs_menu
     ├── testimonial-tabs_link [attribute: "data-tab=X" ; X = 1,2,3,...]
        ├── active-circle [inside the active tab link]
└── testimonial-tabs_content

For Author Images:

testimonial-image_wrap(Parent)
├── testimonial_image [attribute: "data-tab=X" ; X = 1,2,3,...]
└── testimonial_image

Webflow Custom Code Placement

Step 1 — Add GSAP + ScrollTrigger

Place your GSAP CDN scripts in:
Project Settings → Custom Code → Head

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>

Step 2 — Add your custom script

Paste your full code inside:
Project Settings → Custom Code → Footer

<script>

  gsap.registerPlugin(ScrollTrigger);

  document.addEventListener("DOMContentLoaded", () => {
    initTestimonialTabs();
  });

   /* ========== GSAP TESTIMONIAL TABS ========== */
  function initTestimonialTabs() {
    const tabMenu = document.querySelector(".testimonial-tabs_menu");
    const tabs = document.querySelectorAll(".testimonial-tabs_link");
    const activeCircle = document.querySelector(".active-circle");
    const images = document.querySelectorAll(".testimonial-image_wrap .testimonial_image");

    const moveActiveCircle = (target) => {
      const rect = target.getBoundingClientRect();
      const parentRect = tabMenu.getBoundingClientRect();
      const offsetX = rect.left - parentRect.left + rect.width / 2 - activeCircle.offsetWidth / 2;

      gsap.to(activeCircle, {
        x: offsetX,
        duration: 0.4,
        ease: "power2.out"
      });
    };

    const showImage = (tabValue) => {
      images.forEach((img) => {
        const isActive = img.getAttribute("data-tab") === tabValue;
        gsap.to(img, {
          autoAlpha: isActive ? 1 : 0,
          zIndex: isActive ? 10 : 1,
          duration: 0.6,
          ease: "power1.inOut"
        });
      });
    };

    // Initial setup
    const initialTab = document.querySelector(".testimonial-tabs_link.w--current") || tabs[0];
    
    gsap.set(activeCircle, { x: 0 });
    gsap.set(images, { autoAlpha: 0 });
    if (initialTab) {
      moveActiveCircle(initialTab);
      showImage(initialTab.getAttribute("data-tab"));
      gsap.set(`.testimonial_image[data-tab="${initialTab.getAttribute("data-tab")}"]`, { autoAlpha: 1 });
    }

    // Click handlers
    tabs.forEach((tab) => {
      tab.addEventListener("click", () => {
        moveActiveCircle(tab);
        showImage(tab.getAttribute("data-tab"));
      });
    });
  }
  
</script>
  • Moves the active-circle under the active tab smoothly slides to match the clicked tab.

  • When a tab is clicked, GSAP:

    • Finds the matching image using data-tab

    • Fades that image in

    • Fades all others out

    • Brings the active image to the front (zIndex: 10)

  • Detects the default tab

    • On page load, it looks for: .testimonial-tabs_link.w--current

    • Or the first tab if none selected

  • Animates the circle and image instantly on load. So the initial view is correct before any interaction.

How to Add More Tabs in the Future
  • Duplicate a tab link → change data-tab="x"

  • No need to add the active-circle again (only the first one needs it)

  • Duplicate an image → change data-tab="x"

  • The script will automatically pick it up.

Removing Testimonial Tab Switching Animation

If you want to remove the GSAP Testimonial Tab Switching animation from your project, follow these steps:

  • Go to your Site Settings → Custom Code → Footer Code.

  • Find the script below with the comment: GSAP TESTIMONIAL TABS

  • Delete the script that appears below this comment.

  • Remove the custom attributes from your elements:

    • Form tab link → remove attribute [data-tab="x"]

    • Form images → remove attribute [data-tab="x"]

Optimized Code for Both Animation (GSAP)

Below you can see the optimized (dry) code for both counter and testimonial tab switching animation we used in this website:

Webflow Custom Code Placement

Step 1 — Add GSAP + ScrollTrigger

Place your GSAP CDN scripts in:
Project Settings → Custom Code → Head

<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>

Step 2 — Add your custom script

Paste your full code inside:
Project Settings → Custom Code → Footer

<script>

  gsap.registerPlugin(ScrollTrigger);

  document.addEventListener("DOMContentLoaded", () => {
    initTestimonialTabs();
    initCounterAnimations();
  });

  /* ========== GSAP TESTIMONIAL TABS ========== */
  function initTestimonialTabs() {
    const tabMenu = document.querySelector(".testimonial-tabs_menu");
    const tabs = document.querySelectorAll(".testimonial-tabs_link");
    const activeCircle = document.querySelector(".active-circle");
    const images = document.querySelectorAll(".testimonial-image_wrap .testimonial_image");

    const moveActiveCircle = (target) => {
      const rect = target.getBoundingClientRect();
      const parentRect = tabMenu.getBoundingClientRect();
      const offsetX = rect.left - parentRect.left + rect.width / 2 - activeCircle.offsetWidth / 2;

      gsap.to(activeCircle, {
        x: offsetX,
        duration: 0.4,
        ease: "power2.out"
      });
    };

    const showImage = (tabValue) => {
      images.forEach((img) => {
        const isActive = img.getAttribute("data-tab") === tabValue;
        gsap.to(img, {
          autoAlpha: isActive ? 1 : 0,
          zIndex: isActive ? 10 : 1,
          duration: 0.6,
          ease: "power1.inOut"
        });
      });
    };

    // Initial setup
    const initialTab = document.querySelector(".testimonial-tabs_link.w--current") || tabs[0];
    
    gsap.set(activeCircle, { x: 0 });
    gsap.set(images, { autoAlpha: 0 });
    if (initialTab) {
      moveActiveCircle(initialTab);
      showImage(initialTab.getAttribute("data-tab"));
      gsap.set(`.testimonial_image[data-tab="${initialTab.getAttribute("data-tab")}"]`, { autoAlpha: 1 });
    }

    // Click handlers
    tabs.forEach((tab) => {
      tab.addEventListener("click", () => {
        moveActiveCircle(tab);
        showImage(tab.getAttribute("data-tab"));
      });
    });
  }

  /* ========== GSAP COUNTER ========== */
  function initCounterAnimations() {
    const counters = document.querySelectorAll(".counter");

    counters.forEach((counter) => {
      const targetText = counter.textContent.trim();
      const targetNumber = parseFloat(targetText.replace(/[^\d.]/g, "")) || 0;
      const suffix = targetText.replace(/[\d.,\s]/g, "");

      // Start from 0
      gsap.set(counter, { innerText: 0 });

      gsap.to(counter, {
        innerText: targetNumber,
        duration: 3.5,
        ease: "power2.out",
        snap: { innerText: 1 },
        scrollTrigger: {
          trigger: counter,
          start: "top 85%",
          toggleActions: "play none none reset"
        },
        onUpdate: function () {
          const value = Math.ceil(this.targets()[0].innerText);
          counter.innerText = value.toLocaleString("en-US") + suffix;
        }
      });
    });
  }
</script>