Instructions
This template uses GSAP (GreenSock Animation Platform) to create smooth, high-performance animations across multiple sections of the website. All GSAP scripts are written in pure JavaScript and organized for easy customization, allowing you to adjust animation speed, direction, easing, triggers, and timing without affecting the overall structure.
Each animation includes clear comments to help you understand how it works, making it simple to modify or extend the interactions to match your project's needs while remaining fully compatible with Webflow.
1. Lenis Smooth Scroll
<!-- =========================================================
LENIS SMOOTH SCROLL
---------------------------------------------------------
Lenis handles smooth scrolling and is synchronized
with GSAP ScrollTrigger.
========================================================= -->
<script src="https://unpkg.com/lenis@1.3.4/dist/lenis.min.js"></script>
<link
rel="stylesheet"
href="https://unpkg.com/lenis@1.3.4/dist/lenis.css"
/>
<script>
// Initialize Lenis smooth scrolling
const lenis = new Lenis({
smooth: true,
lerp: 0.1,
wheelMultiplier: 0.75,
infinite: false,
});
// Keep ScrollTrigger synchronized with Lenis
lenis.on("scroll", ScrollTrigger.update);
// Run Lenis through the GSAP ticker
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
// Disable GSAP lag smoothing for consistent scroll synchronization
gsap.ticker.lagSmoothing(0);
</script>A. Overview & Description
This script integrates Lenis, a modern smooth-scrolling library, with GSAP (GreenSock Animation Platform) and ScrollTrigger. It provides an ultra-smooth, high-performance scrolling experience while ensuring all scroll-driven animations stay perfectly synchronized. By routing Lenis's requestAnimationFrame (raf) through GSAP's internal ticker and disabling lag smoothing, the setup prevents jitter and maintains precise scroll position tracking across all devices.
Key Features:
- Smooth Scrolling: Delivers continuous, fluid scrolling using inertia (lerp: 0.1).
- GSAP Synchronization: Keeps ScrollTrigger updates aligned with Lenis's custom scroll frame.
- Lag Smoothing Override: Eliminates animation jumps during sudden frame drops or window re-focusing.
B. How to Edit GSAP Animations
1) Element Map
Below is a reference guide mapping the core JavaScript objects and selectors initialized in this script:
Key Features:
- lenis – Initializes the Lenis smooth-scroll instance with configuration settings (lerp: 0.1, wheelMultiplier: 0.75, infinite: false).
- ScrollTrigger.update – Synchronizes GSAP's ScrollTrigger with Lenis every time a scroll event occurs (lenis.on("scroll", ...)).
- gsap.ticker – Drives Lenis frame updates (lenis.raf) directly within GSAP's render loop and disables lag smoothing (gsap.ticker.lagSmoothing(0)).
2) Customizing Key Variables
You can adjust the smooth scroll performance and GSAP integration behavior directly in the script using these key parameters:
- Lenis Smooth Scroll Settings:
const lenis = new Lenis({ smooth: true, // Enables/disables smooth scrolling lerp: 0.1, // Scroll interpolation/smoothness (lower values = smoother/slower catch-up) wheelMultiplier: 0.75, // Mouse wheel scroll speed multiplier infinite: false, // Enables or disables infinite looping scroll }); - GSAP Ticker & Lag Smoothing:
// Disables GSAP's lag smoothing to ensure GSAP and Lenis tick on the exact same frame gsap.ticker.lagSmoothing(0);
3) Removing GSAP Animations
If you want to modify or remove the GSAP synchronization while keeping Lenis smooth scroll, follow these steps:
a) Step-by-Step Disable Instructions:
- To disable GSAP synchronization entirely while keeping basic Lenis smooth scrolling, remove or comment out the GSAP integration lines:
// Remove or comment out these lines: // lenis.on("scroll", ScrollTrigger.update); // gsap.ticker.add((time) => { lenis.raf(time * 1000); }); // gsap.ticker.lagSmoothing(0); - Replace the ticker update with standard requestAnimationFrame logic to keep Lenis running independently:
function raf(time) { lenis.raf(time); requestAnimationFrame(raf); } requestAnimationFrame(raf);
b) Visual Side Effects & Considerations:
- Scroll Trigger Desync: Disabling ScrollTrigger.update or removing the GSAP ticker sync will cause scroll-based GSAP animations to jitter, lag, or fail to sync accurately with the smooth scroll position.
- Performance Shifts: Removing gsap.ticker.lagSmoothing(0) returns GSAP to its default frame-skipping behavior during heavy page loads, which can cause subtle jumps in scroll animations.
2. Number Counting
<!-- =========================================================
GSAP NUMBER COUNTING
---------------------------------------------------------
Target:
Elements with the ".is-counting" class
Features:
- Counts numbers from 0 to the original value
- Supports prefixes and suffixes
- Supports integer and decimal values
- Plays when ".is-counting" is added
- Reverses when ".is-counting" is removed
- Works with Webflow Interactions through MutationObserver
========================================================= -->
<script>
window.addEventListener("DOMContentLoaded", () => {
gsap.registerPlugin(ScrollTrigger);
// Store each counting tween for later control
const countingTweens = new Map();
// Initialize the counting animation for a single element
function initCounting(element) {
// Prevent duplicate initialization
if (element.dataset.countingInitialized) return;
element.dataset.countingInitialized = "true";
// Read the original text content
const text = element.textContent.trim();
// Extract prefix, numeric value, and suffix
const match = text.match(/^([^\d]*)([\d.]+)(.*)$/);
if (!match) return;
const prefix = match[1] || "";
const value = parseFloat(match[2]);
const suffix = match[3] || "";
// Starting value for the counter
const counter = {
value: 0,
};
// Create the counting tween
const tween = gsap.to(counter, {
value: value,
duration: 1.8,
ease: "power2.out",
paused: true,
// Snap integers to whole numbers and decimals to one decimal place
snap: {
value: Number.isInteger(value) ? 1 : 0.1,
},
// Update the displayed number on every frame
onUpdate() {
const currentValue = Number.isInteger(value)
? Math.round(counter.value)
: counter.value.toFixed(1);
element.textContent = `${prefix}${currentValue}${suffix}`;
},
});
// Trigger the counter when the element enters the viewport
ScrollTrigger.create({
trigger: element,
start: "top 100%",
toggleActions: "play none play reverse",
animation: tween,
});
// Store the tween for manual play/reverse control
countingTweens.set(element, tween);
}
// Initialize elements that already have ".is-counting"
document.querySelectorAll(".is-counting").forEach(initCounting);
// Observe class changes caused by Webflow Interactions
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.type === "attributes" &&
mutation.attributeName === "class"
) {
const target = mutation.target;
// Start counting when ".is-counting" is added
if (target.classList.contains("is-counting")) {
initCounting(target);
const tween = countingTweens.get(target);
if (tween) {
tween.play();
}
}
// Reverse counting when ".is-counting" is removed
else {
const tween = countingTweens.get(target);
if (tween) {
tween.reverse();
}
}
}
});
});
// Monitor class changes throughout the entire document
observer.observe(document.body, {
attributes: true,
attributeFilter: ["class"],
subtree: true,
});
});
</script>A. Overview & Description
This script creates animated numerical counters using GSAP and ScrollTrigger. It targets any element with the .is-counting class and animates its text value from 0 up to its original number when scrolled into view. Additionally, it integrates a MutationObserver to watch for class changes dynamically, allowing Webflow Interactions to toggle or reverse the counting animation seamlessly.
Key Features:
- Automatic Number Parsing: Detects integers, decimals, prefixes (e.g., $, +), and suffixes (e.g., %, k+) directly from the text content.
- Scroll-Triggered Playback: Animates numbers as soon as they enter the viewport (start: "top 100%").
- Webflow Interaction Support: Replays or reverses counting when the .is-counting class is dynamically added or removed.
B. How to Edit GSAP Animations
1) Element Map
Below is a reference guide mapping the core selectors and functionality initialized in this script:
- .is-counting – The target CSS class applied to text elements that should animate from 0 to their designated number.
- Triggering via Webflow Interactions Timeline: Play / Start Counter: Add the is-counting class to the target element in your interaction timeline. Reverse / Reset Counter: Remove the is-counting class from the target element in your interaction timeline.
- countingTweens – A JavaScript Map that stores individual tween references to allow independent play and reverse controls per element.
- MutationObserver – Monitors document.body for class attribute changes to sync animations with dynamic Webflow interactions.
2) Customizing Key Variables
You can customize the counter speed, easing, and scroll sensitivity directly inside the initCounting() function:
- Duration & Easing:
const tween = gsap.to(counter, { value: value, duration: 1.8, // Animation length in seconds ease: "power2.out", // Acceleration curve (e.g., "power1.out", "expo.out") paused: true, // Snapping configuration for whole numbers vs decimals snap: { value: Number.isInteger(value) ? 1 : 0.1, }, // ... }); - ScrollTrigger Settings:
ScrollTrigger.create({ trigger: element, start: "top 100%", // Triggers as soon as the element hits the bottom of the screen toggleActions: "play none play reverse", // Controls play/reverse behavior on scroll entry/exit animation: tween, });
3) Removing GSAP Animations
If you want to disable or remove the number counting animation, follow these steps:
a) Step-by-Step Disable Instructions:
- Locate and remove or comment out the <script> block containing the GSAP Number Counting code.
- Remove the .is-counting combo class from your Webflow text elements or Webflow Interaction triggers if no longer needed.
b) Visual Side Effects & Considerations:
- Static Display: Removing the script causes numbers to display as static text (e.g., "100%", "$250") immediately upon page load without counting up.
- No Layout Shifts: Because the script reads original text node values before animating, removing it will not break layout dimensions or styling.
3. Infinite Draggable Marquee
<!-- =========================================================
GSAP INFINITE DRAGGABLE MARQUEE
---------------------------------------------------------
Target:
Elements with the '[wb-data="marquee"]' attribute
Features:
- Clones content automatically for a seamless infinite loop
- Supports custom speed via 'duration' attribute
- Smooth pause on hover and play on leave
- Draggable & Inertia interaction with progress mapping
- Item scaling feedback during drag/press interaction
- Auto-recalculates track width on window resize (debounced)
========================================================= -->
<script>
window.addEventListener("DOMContentLoaded", () => {
// Register GSAP Plugins
gsap.registerPlugin(Draggable, InertiaPlugin);
const initMarquee = () => {
// Find the marquee wrapper element
const marquee = document.querySelector('[wb-data="marquee"]');
if (!marquee) return;
// Extract duration attribute or fallback to default
const duration = parseInt(marquee.getAttribute("duration"), 10) || 5;
const marqueeContent = marquee.firstChild;
if (!marqueeContent) return;
// Clone content for seamless infinite looping
const marqueeContentClone = marqueeContent.cloneNode(true);
marquee.append(marqueeContentClone);
// Apply initial grab cursor style
marquee.style.cursor = "grab";
let tween;
let distanceToTranslate;
// Initialize or refresh marquee animation
const playMarquee = () => {
let progress = tween ? tween.progress() : 0;
if (tween) tween.progress(0).kill();
// Calculate translation distance based on content width and gap
const width = parseInt(getComputedStyle(marqueeContent).getPropertyValue("width"), 10);
const gap = parseInt(getComputedStyle(marqueeContent).getPropertyValue("column-gap"), 10);
distanceToTranslate = -1 * (gap + width);
// Create infinite timeline
tween = gsap.fromTo(
marquee.children,
{ x: 0 },
{
x: distanceToTranslate,
duration: duration,
ease: "none",
repeat: -1,
}
);
tween.progress(progress);
};
playMarquee();
// Pause playback on mouse enter
marquee.addEventListener("mouseenter", () => {
if (tween) gsap.to(tween, { timeScale: 0, duration: 0.3 });
});
// Resume playback on mouse leave
marquee.addEventListener("mouseleave", () => {
if (tween) gsap.to(tween, { timeScale: 1, duration: 0.3 });
});
// Proxy element used by Draggable to control progress
const proxy = document.createElement("div");
let startProgress = 0;
// Configure Draggable instance
Draggable.create(proxy, {
type: "x",
trigger: marquee,
inertia: true,
onPressInit() {
// Pause marquee animation during user interaction
gsap.killTweensOf(tween);
tween.timeScale(0);
marquee.style.cursor = "grabbing";
startProgress = tween.progress();
gsap.set(proxy, { x: 0 });
// Scale down horizon items on interaction start
gsap.to(".item-horizon-block", { scale: 0.95, duration: 0.2 });
},
onDrag() {
// Map drag distance to continuous progress (0 to 1 loop)
const progressDelta = this.x / distanceToTranslate;
let newProgress = startProgress + progressDelta;
newProgress = ((newProgress % 1) + 1) % 1;
tween.progress(newProgress);
},
onThrowUpdate() {
// Map inertia drag distance during momentum throw
const progressDelta = this.x / distanceToTranslate;
let newProgress = startProgress + progressDelta;
newProgress = ((newProgress % 1) + 1) % 1;
tween.progress(newProgress);
},
onRelease() {
marquee.style.cursor = "grab";
// Reset item scale back to normal
gsap.to(".item-horizon-block", { scale: 1, duration: 0.3 });
},
onThrowComplete() {
// Resume autoplay according to current hover state
const isHovering = marquee.matches(":hover");
gsap.to(tween, { timeScale: isHovering ? 0 : 1, duration: 0.3 });
},
});
// Debounce utility to prevent high-frequency recalculations on resize
function debounce(func, delay = 500) {
let timer;
return function (...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), delay);
};
}
// Recalculate track position when viewport resizes
window.addEventListener("resize", debounce(playMarquee));
};
initMarquee();
});
</script>A. Overview & Description
This script creates an infinite horizontal marquee using GSAP. It automatically clones the first content block for a seamless loop, pauses on hover, resumes on mouse leave, and supports drag interaction with inertia.
Key Features:
- Infinite Loop: Clones the first content block and translates both children continuously for a seamless repeating marquee.
- Custom Speed: Uses the duration attribute to control autoplay speed, with a default value of 5 seconds.
- Hover, Drag & Inertia: Pauses on hover and allows manual dragging with momentum through Draggable and InertiaPlugin.
B. How to Edit GSAP Animations
1) Element Map
Below is a reference guide mapping the core selectors and functionality initialized in this script:
- [wb-data="marquee"] – The main wrapper targeted by the script. It controls autoplay, hover pause, cursor states, and drag interaction.
- duration – Optional marquee speed attribute. If omitted, the script falls back to 5 seconds.
- marqueeContent / marqueeContentClone – The original first child and its automatic clone used to create the seamless loop.
- proxy – A temporary element controlled by Draggable. Its x position is mapped to the marquee tween progress during drag and inertia.
2) Customizing Key Variables
You can customize the marquee speed, easing, drag feedback, and resize debounce directly in the script:
- Duration & Easing:
const tween = gsap.to(counter, { value: value, duration: 1.8, // Animation length in seconds ease: "power2.out", // Acceleration curve (e.g., "power1.out", "expo.out") paused: true, // Snapping configuration for whole numbers vs decimals snap: { value: Number.isInteger(value) ? 1 : 0.1, }, // ... }); - ScrollTrigger Settings:
ScrollTrigger.create({ trigger: element, start: "top 100%", // Triggers as soon as the element hits the bottom of the screen toggleActions: "play none play reverse", // Controls play/reverse behavior on scroll entry/exit animation: tween, });
If you want to disable or remove the Infinite Draggable Marquee animation, follow these steps:
If you want to disable or remove the number counting animation, follow these steps:
a) Step-by-Step Disable Instructions:
- Locate and remove or comment out the <script> block containing the GSAP Infinite Draggable Marquee code.
- Remove the wb-data="marquee" attribute and the duration attribute from the marquee wrapper if the animation is no longer needed.
b) Visual Side Effects & Considerations:
- Static Display: Removing the script leaves the existing marquee content in its normal static layout without GSAP movement or drag behavior.
- No layout changes are required when disabling the script; the existing content and Webflow styles remain intact.
