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.
<!-- =========================================================
     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>