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.
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.
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:
counterType your final number inside the text (example:
450+,2,500,90%, etc.)
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
.counterand extracts the number part.It resets the counter to 0 before the animation starts.
Using GSAP, it smoothly counts from 0 → your 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.
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.
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
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-tabFades 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.
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.
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:
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>