INSTRUCTION

Webflow Template User Guide

GSAP Guide
Every GSAP code used on this template is here. How to edit them and find them is explain on this page. In every code block on this page, we added additional explanation to help you understand everything.

You can find the code in (Site settings) Footer Code.
Lenis Smooth Scroll
<script src="https://unpkg.com/lenis@1.3.1/dist/lenis.min.js"></script> 

 

<script>

// lenis smooth scroll
  
{
  let lenis;

  const initScroll = () => {
    lenis = new Lenis({});
    lenis.on("scroll", ScrollTrigger.update);
    gsap.ticker.add((time) => lenis.raf(time * 1000));
    gsap.ticker.lagSmoothing(0);
  };

  function initGsapGlobal() {
    
    // Do everything that needs to happen
    //  before triggering all
    // the gsap animations 

    initScroll();

    // match reduced motion media
    // const media = gsap.matchMedia();

    // Send a custom
    //  event to all your
    // gsap animations
    // to start them 

    const sendGsapEvent = () => {
      window.dispatchEvent(
        new CustomEvent("GSAPReady", {
          detail: {
            lenis,
          },
        })
      );
    };

    // Check if fonts are already loaded
    
    if (document.fonts.status === "loaded") {
      sendGsapEvent();
    } else {
      document.fonts.ready.then(() => {
        sendGsapEvent();
      });
    }

    // We need specific handling because the
    // grid/list changes the scroll height of the whole container
    //

    let resizeTimeout;
    const onResize = () => {
      clearTimeout(resizeTimeout);
      resizeTimeout = setTimeout(() => {
        ScrollTrigger.refresh();
      }, 50);
    };

    window.addEventListener("resize", () => onResize());
    const resizeObserver = new ResizeObserver((entries) => onResize());
    resizeObserver.observe(document.body);

    queueMicrotask(() => {
      gsap.to("[data-start='hidden']", {
        autoAlpha: 1,
        duration: 0.1,
        delay: 0.2,
      });
    });
  }

  // this only for dev
  
  const documentReady =
    document.readyState === "complete" || document.readyState === "interactive";

  if (documentReady) {
    initGsapGlobal();
  } else {
    addEventListener("DOMContentLoaded", (event) => initGsapGlobal());
  }
}

</script>
Lenis Smooth Scroll is a lightweight JavaScript library that enables buttery-smooth, hardware-accelerated scrolling for websites. It works by intercepting the browser’s native scroll behavior and applying eased, customizable motion, creating a more fluid and refined browsing experience. Lenis supports vertical and horizontal scroll, inertia effects, and syncs seamlessly with animations from libraries like GSAP or ScrollTrigger.
Hero Text Bg Animation
GSAP animation of moving hero text background
<script>
//Hero Text Bg Animation
  gsap.to(".big-text.v1", {
    backgroundPosition: "200% center",
    duration: 20,
    ease: "none",
    repeat: -1,
    yoyo: true
  });

  gsap.to(".big-text.v2", {
    backgroundPosition: "200% center",
    duration: 20,
    ease: "nonet",
    repeat: -1,
    yoyo: true
  });

  gsap.to(".big-text.tight.v-1", {
    backgroundPosition: "200% center",
    duration: 20,
    ease: "none",
    repeat: -1,
    yoyo: true
  });

  gsap.to(".big-text.tight.v-2", {
    backgroundPosition: "200% center",
    duration: 20,
    ease: "none",
    repeat: -1,
    yoyo: true
  });

  gsap.to(".big-text.tight.v-3", {
    backgroundPosition: "200% center",
    duration: 20,
    ease: "none",
    repeat: -1,
    yoyo: true
  });
</script>
Hero Image Trail Animation
GSAP animation for following cursor image with trailing effect
<script>
// Hero Image Trail Effect (Mouse + Touch Support)
document.addEventListener("DOMContentLoaded", () => {

  // --- ELEMENTS & INITIAL SETUP ---
  const wrapper = document.querySelector(".image-wrap");
  if (!wrapper) return console.error("Missing .image-wrap container");

  const images = gsap.utils.toArray(".content-img-wrap");

  let mouse = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
  let last = { x: mouse.x, y: mouse.y };
  let cache = { x: mouse.x, y: mouse.y };
  let index = 0;
  let threshold = 80;
  let activeImages = 0;
  let idle = true;

  // --- MOUSE MOVEMENT TRACKING ---
  window.addEventListener("mousemove", (e) => {
    mouse.x = e.clientX;
    mouse.y = e.clientY;
  });

  // --- TOUCH SUPPORT: TAP + SWIPE ---
  window.addEventListener("touchstart", (e) => {
    const touch = e.touches[0];
    mouse.x = touch.clientX;
    mouse.y = touch.clientY;
    showNextImage(); // trigger image on tap
  });

  window.addEventListener("touchmove", (e) => {
    const touch = e.touches[0];
    mouse.x = touch.clientX;
    mouse.y = touch.clientY;
  });

  // --- GSAP TICKER LOOP ---
  gsap.ticker.add(() => {

    // Smooth follow (lerp effect)
    cache.x = gsap.utils.interpolate(cache.x, mouse.x, 0.1);
    cache.y = gsap.utils.interpolate(cache.y, mouse.y, 0.1);

    // Distance check
    const dist = Math.hypot(mouse.x - last.x, mouse.y - last.y);

    if (dist > threshold) {
      showNextImage();
      last.x = mouse.x;
      last.y = mouse.y;
    }

    // Reset index when idle
    if (idle && index !== 1) index = 1;
  });

  // --- SHOW NEXT IMAGE FUNCTION ---
  function showNextImage() {
    index++;
    const img = images[index % images.length];
    const rect = img.getBoundingClientRect();

    gsap.killTweensOf(img);

    gsap.set(img, {
      opacity: 0,
      scale: 0,
      zIndex: index,
      x: cache.x - rect.width / 2,
      y: cache.y - rect.height / 2
    });

    // Image trail animation
    gsap.timeline({
      onStart: () => { activeImages++; idle = false; },
      onComplete: () => {
        activeImages--;
        if (activeImages === 0) idle = true;
      }
    })
    .to(img, {
      duration: 0.4,
      opacity: 1,
      scale: 1,
      x: mouse.x - rect.width / 2,
      y: mouse.y - rect.height / 2,
      ease: "power2.out"
    })
    .to(img, {
      duration: 0.8,
      opacity: 0,
      scale: 0.2,
      ease: "power2.in",
      x: cache.x - rect.width / 2,
      y: cache.y - rect.height / 2
    }, "+=0.3");
  }

});
</script>
SVG Filter + GSAP Glitch hover effect
Animation effect for hovering work/portfolio card
<!--SVG Filter CODE-->
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
  <filter id="cyberGlitch">
    <feTurbulence id="glitchNoise"
      type="fractalNoise"
      baseFrequency="0.02 0.8"
      numOctaves="1"
      seed="3"
      result="noise" />
    <feDisplacementMap id="glitchDisp"
      in="SourceGraphic"
      in2="noise"
      scale="0"
      xChannelSelector="R"
      yChannelSelector="G"
      result="displaced" />
    <feOffset id="glitchRGB"
      in="displaced"
      dx="0"
      dy="0"
      result="rgbShift" />
    <feBlend in="displaced" in2="rgbShift" mode="lighten" result="blended" />
    <feComponentTransfer in="blended" result="final">
      <feFuncR type="linear" slope="1.0" />
      <feFuncG type="linear" slope="1.0" />
      <feFuncB type="linear" slope="1.0" />
    </feComponentTransfer>
  </filter>
</svg>
<script>
//Glitch Effect
document.addEventListener("DOMContentLoaded", () => {
  const cursor = document.querySelector('.cursor-pointer');
  const cards = document.querySelectorAll('.single-project-card');

  // SVG filter nodes
  const turb = document.querySelector('#glitchNoise');
  const disp = document.querySelector('#glitchDisp');
  const rgb = document.querySelector('#glitchRGB');

  if (!cursor || !turb || !disp) {
    console.warn('Missing selector: .cursor-pointer or #glitchNoise/#glitchDisp. Check HTML.');
    return;
  }

  // create a reusable timeline (paused)
  const glitchTL = gsap.timeline({ paused: true, defaults: { ease: "power2.inOut" } })
    .to(turb, { attr: { baseFrequency: "0.05 1.2" }, duration: 0.12 })
    .to(disp, { attr: { scale: 60 }, duration: 0.12 }, "<")
    .to(rgb, { attr: { dx: 6 }, duration: 0.12 }, "<")
    .to(turb, { attr: { baseFrequency: "0.02 0.6" }, duration: 0.2 })
    .to(disp, { attr: { scale: 30 }, duration: 0.2 }, "<")
    .to(rgb, { attr: { dx: 3 }, duration: 0.2 }, "<")
    .to(turb, { attr: { baseFrequency: "0 0" }, duration: 0.25 })
    .to(disp, { attr: { scale: 0 }, duration: 0.25 }, "<")
    .to(rgb, { attr: { dx: 0 }, duration: 0.25 }, "<");

  // helper: reset filter attributes to "rest" state
  function resetFilterAttrs() {
    gsap.set(turb, { attr: { baseFrequency: "0 0" } });
    gsap.set(disp, { attr: { scale: 0 } });
    gsap.set(rgb, { attr: { dx: 0 } });
  }

  // ensure initial state
  resetFilterAttrs();

  cards.forEach(card => {
    card.addEventListener('mouseenter', () => {
      // 1) Make sure cursor has no filter (force reflow)
      cursor.style.filter = 'none';
      // Force reflow/read of layout to ensure removal is processed
      // eslint-disable-next-line no-unused-expressions
      cursor.offsetWidth;

      // 2) Reset filter attributes (so animation always starts from same baseline)
      resetFilterAttrs();

      // 3) Re-apply the filter to the cursor
      cursor.style.filter = 'url(#cyberGlitch)';

      // 4) If timeline is active, restart it (restart(true) forces immediate restart)
      //    If it's currently playing, this will jump it back to start cleanly.
      glitchTL.restart(true);
    });

    card.addEventListener('mouseleave', () => {
      // optional: remove filter when leaving (so it only shows while hovered / animating)
      // small timeout helps the timeline finish smoothing (tweak or remove as you like)
      gsap.to({}, { duration: 0.05, onComplete: () => cursor.style.filter = 'none' });
    });
  });

  // Optional: stop everything on page visibility change to avoid stuck state
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
      glitchTL.pause(0);
      resetFilterAttrs();
      if (cursor) cursor.style.filter = 'none';
    }
  });
});

</script>
<script>
// TOUCHSCREEN CYBERGLITCH
document.addEventListener("DOMContentLoaded", () => {

  // ====== ELEMENT REFERENCES ======
  const triggers = document.querySelectorAll(".single-project-wrap");
  const turb = document.querySelector("#glitchNoise");
  const disp = document.querySelector("#glitchDisp");
  const rgb  = document.querySelector("#glitchRGB");

  // ====== CHECK SVG FILTER NODES ======
  if (!turb || !disp || !rgb) {
    console.warn("⚠️ Missing SVG filter parts (#glitchNoise, #glitchDisp, #glitchRGB)");
    return;
  }

  // ====== GLITCH TIMELINE ======
  const glitchTL = gsap.timeline({
    paused: true,
    defaults: { ease: "power2.inOut" }
  })
  .to(turb, { attr: { baseFrequency: "0.05 1.2" }, duration: 0.12 })
  .to(disp, { attr: { scale: 60 }, duration: 0.12 }, "<")
  .to(rgb,  { attr: { dx: 6 }, duration: 0.12 }, "<")

  .to(turb, { attr: { baseFrequency: "0.02 0.6" }, duration: 0.2 })
  .to(disp, { attr: { scale: 30 }, duration: 0.2 }, "<")
  .to(rgb,  { attr: { dx: 3 }, duration: 0.2 }, "<")

  .to(turb, { attr: { baseFrequency: "0 0" }, duration: 0.25 })
  .to(disp, { attr: { scale: 0 }, duration: 0.25 }, "<")
  .to(rgb,  { attr: { dx: 0 }, duration: 0.25 }, "<");

  // ====== RESET FILTER ATTRIBUTES ======
  function resetFilterAttrs() {
    gsap.set(turb, { attr: { baseFrequency: "0 0" } });
    gsap.set(disp, { attr: { scale: 0 } });
    gsap.set(rgb,  { attr: { dx: 0 } });
  }

  resetFilterAttrs();

  // ====== PLAY GLITCH ON TARGET ELEMENT ======
  function playGlitch(el) {
    el.style.filter = "url(#cyberGlitch)";
    glitchTL.restart(true);

    gsap.delayedCall(glitchTL.duration(), () => {
      el.style.filter = "none";
      resetFilterAttrs();
    });
  }

  // ====== VIEWPORT CHECK ======
  function isTabletOrBelow() {
    return window.innerWidth <= 991;
  }

  // ====== EVENT LISTENERS ======
  triggers.forEach(trigger => {
    const mobileButton = trigger.querySelector(".mobile-button");
    if (!mobileButton) return; // Skip if no button found

    // Touch devices
    trigger.addEventListener("touchstart", e => {
      if (isTabletOrBelow()) playGlitch(mobileButton);
    }, { passive: true });

    // Fallback for desktop testing
    trigger.addEventListener("click", e => {
      if (isTabletOrBelow()) playGlitch(mobileButton);
    });
  });

});
</script>