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.
Strada Custom Code
Technical Instructions:
This script is designed to run in Webflow → Project Settings → Custom Code → Footer Code, which is automatically placed at the end of the `<body>` tag. The required GSAP dependencies (GSAP, ScrollTrigger, and ScrambleTextPlugin) must already be loaded before this script. The script controls hover scramble text, number counting, the award accordion, and Lenis smooth scrolling. Keep the existing Webflow class names and element structure unchanged, especially `.hover-effect`, `.is-counting`, `.award-details`, `.award-title-wrap`, `.award-details-wrap`, `.icon-open`, and `.icon-close`, as these selectors are required for the interactions to function correctly. Lenis is loaded from its CDN and synchronized with GSAP ScrollTrigger for smooth scrolling and scroll-based animations. No additional initialization or manual trigger is required.
This script is designed to run in Webflow → Project Settings → Custom Code → Footer Code, which is automatically placed at the end of the `<body>` tag. The required GSAP dependencies (GSAP, ScrollTrigger, and ScrambleTextPlugin) must already be loaded before this script. The script controls hover scramble text, number counting, the award accordion, and Lenis smooth scrolling. Keep the existing Webflow class names and element structure unchanged, especially `.hover-effect`, `.is-counting`, `.award-details`, `.award-title-wrap`, `.award-details-wrap`, `.icon-open`, and `.icon-close`, as these selectors are required for the interactions to function correctly. Lenis is loaded from its CDN and synchronized with GSAP ScrollTrigger for smooth scrolling and scroll-based animations. No additional initialization or manual trigger is required.
<!-- =========================================================
GSAP HOVER SCRAMBLE TEXT
---------------------------------------------------------
Target:
Any element with a class containing "hover-effect"
Features:
- Cleans unnecessary whitespace from Webflow text
- Locks the element width to prevent layout shifting
- Scrambles text on hover
- Restores original text on mouse leave
- Kills previous tweens to prevent overlapping animations
========================================================= -->
<script>
document.addEventListener("DOMContentLoaded", () => {
gsap.registerPlugin(ScrambleTextPlugin);
document.querySelectorAll('[class*="hover-effect"]').forEach((element) => {
// Get clean text content and remove unnecessary whitespace
const cleanText = element.textContent.replace(/\s+/g, " ").trim();
// Lock the current physical width to prevent layout shifting
const currentWidth = element.getBoundingClientRect().width;
element.style.width = `${currentWidth}px`;
// Store the current scramble tween
let scrambleTween;
// Start scramble animation on hover
element.addEventListener("mouseenter", () => {
// Kill any previous animation before starting a new one
if (scrambleTween) {
scrambleTween.kill();
}
scrambleTween = gsap.to(element, {
duration: 0.6,
scrambleText: {
text: cleanText,
chars: "upperAndLowerCase",
speed: 1,
tweenLength: false,
},
ease: "power2.out",
});
});
// Restore the original text when the pointer leaves
element.addEventListener("mouseleave", () => {
if (scrambleTween) {
scrambleTween.kill();
}
element.textContent = cleanText;
});
});
});
</script>
<!-- =========================================================
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>
<!-- =========================================================
AWARD ACCORDION
---------------------------------------------------------
Structure:
.award-details
├── .award-title-wrap
├── .award-details-wrap
├── .icon-open
└── .icon-close
Features:
- Only one accordion item can be open at a time
- Smooth height and opacity animation
- Automatically triggers number counting
- Toggles active title state
- Toggles open/close icons
========================================================= -->
<script>
document.addEventListener("DOMContentLoaded", () => {
const accordionItems = document.querySelectorAll(".award-details");
// Stop if no accordion items exist on the page
if (accordionItems.length === 0) return;
// ---------------------------------------------------------
// Initial Setup
// ---------------------------------------------------------
// Collapse all accordion content on page load
// and hide all close icons
accordionItems.forEach((item) => {
const content = item.querySelector(".award-details-wrap");
const iconClose = item.querySelector(".icon-close");
if (content) {
gsap.set(content, {
height: "0vw",
opacity: 0,
visibility: "hidden",
});
}
if (iconClose) {
gsap.set(iconClose, {
opacity: 0,
visibility: "hidden",
});
}
});
// ---------------------------------------------------------
// Accordion Interaction
// ---------------------------------------------------------
accordionItems.forEach((accordion) => {
// Use the title wrapper as the trigger,
// or fall back to the accordion itself
const trigger =
accordion.querySelector(".award-title-wrap") || accordion;
trigger.addEventListener("click", (event) => {
event.stopPropagation();
const currentContent = accordion.querySelector(
".award-details-wrap"
);
const currentIconOpen = accordion.querySelector(".icon-open");
const currentIconClose = accordion.querySelector(".icon-close");
const currentTitle = accordion.querySelector(".award-title-wrap");
// Stop if the current accordion has no content
if (!currentContent) return;
const isOpen = currentContent.classList.contains("is-open");
// -------------------------------------------------------
// Close all other accordion items
// -------------------------------------------------------
accordionItems.forEach((item) => {
const content = item.querySelector(".award-details-wrap");
const iconOpen = item.querySelector(".icon-open");
const iconClose = item.querySelector(".icon-close");
const title = item.querySelector(".award-title-wrap");
// Only process currently open items
if (content && content.classList.contains("is-open")) {
content.classList.remove("is-open");
// Remove active title state
if (title) {
title.classList.remove("is-active");
}
// Remove counting state from numbers
item.querySelectorAll(".text-m, .text-l").forEach((countElement) => {
countElement.classList.remove("is-counting");
});
// Animate content collapse
gsap.to(content, {
height: "0vw",
opacity: 0,
duration: 0.35,
ease: "power2.inOut",
onComplete: () => {
gsap.set(content, {
visibility: "hidden",
});
},
});
// Hide close icon
if (iconClose) {
gsap.to(iconClose, {
opacity: 0,
duration: 0.2,
onComplete: () => {
gsap.set(iconClose, {
visibility: "hidden",
});
},
});
}
// Show open icon
if (iconOpen) {
gsap.set(iconOpen, {
visibility: "visible",
});
gsap.to(iconOpen, {
opacity: 1,
duration: 0.2,
});
}
}
});
// -------------------------------------------------------
// Open clicked accordion item
// -------------------------------------------------------
if (!isOpen) {
currentContent.classList.add("is-open");
// Add active title state
if (currentTitle) {
currentTitle.classList.add("is-active");
}
// Trigger number counting inside the opened item
accordion
.querySelectorAll(".text-m, .text-l")
.forEach((countElement) => {
countElement.classList.add("is-counting");
});
// Temporarily set height to auto
// so the natural content height can be measured
gsap.set(currentContent, {
visibility: "visible",
height: "auto",
});
const targetHeight = currentContent.offsetHeight;
// Reset to collapsed state before animating
gsap.set(currentContent, {
height: "0vw",
});
// Animate accordion expansion
gsap.to(currentContent, {
height: targetHeight,
opacity: 1,
duration: 0.4,
ease: "power2.out",
onComplete: () => {
// Allow responsive content to determine its own height
gsap.set(currentContent, {
height: "auto",
});
},
});
// -----------------------------------------------------
// Icon State
// -----------------------------------------------------
// Hide open icon
if (currentIconOpen) {
gsap.to(currentIconOpen, {
opacity: 0,
duration: 0.2,
onComplete: () => {
gsap.set(currentIconOpen, {
visibility: "hidden",
});
},
});
}
// Show close icon
if (currentIconClose) {
gsap.set(currentIconClose, {
visibility: "visible",
});
gsap.to(currentIconClose, {
opacity: 1,
duration: 0.2,
});
}
}
});
});
});
</script>
<!-- =========================================================
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>