Instruction

Guide for Mellio Webflow Template

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

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

Hero Section GSAP Animation

GSAP animation featuring random splitText with blur effect and expanding flickering background image.
<script>

// -----HERO SECTION------
  
// ---Hero title animation--- 
  
const split = new SplitText(".hero-title", { type: "chars" });

// Set initial opacity to 0 for all characters
  
gsap.set(split.chars, { 
  opacity: 0, 
  filter : "blur(1vw)"
});

  // Animate characters in
gsap.to(split.chars, {
  opacity: 1,
  filter : "blur(0vw)",
  duration: 1,
    stagger: {
    each: 0.8 / split.chars.length,
    from: "random"
  },
  ease: "power2.out"
});
// END hero title animation'
  
  
// Hero Image Flicker
  document.addEventListener("DOMContentLoaded", () => {
  const frame = document.querySelector(".background-image-frame");
  const images = gsap.utils.toArray(".hero-background-image");

  // Show first image initially
  gsap.set(images, { autoAlpha: 0 });
  gsap.set(images[0], { autoAlpha: 1 });

  // ---- Animation 1: Image sequence ----
  gsap.to(images, {
    delay: 0.75, // wait before animation starts
    autoAlpha: 1,
    duration: 0.15,
    stagger: { each: 0.15, from: "start" },
    ease: "power1.inOut",
    onStart: () => {
      // Hide first image just before starting sequence
      gsap.set(images[0], { autoAlpha: 0 });
    },
    onComplete: () => {
      gsap.set(images.slice(0, -1), { autoAlpha: 0 });
      gsap.set(images[images.length - 1], { autoAlpha: 1 });
    }
  });

  // ---- Animation 2: Frame growth ----
  gsap.set(frame, { width: "20vw", height: "20vw" });
  gsap.to(frame, {
    delay: 0.75, // same delay
    width: "100vw",
    height: "100vh",
    duration: 2,
    ease: "power2.inOut"
  });
});
// END Hero Image Flicker

</script>

About Section GSAP Animation

GSAP animation with splitText effect to reveal a paragraph in random letter sequences, tailored with CSS to fix the word wrapping issue that cause the text to not breaking properly when GSAP splitText used on long paragraph. Also featuring trail image that follow the cursor while user navigate the section.
<style>
/* SplitText character style Bug fix for word wrapping */
    .split-about-word {
    display: inline-block;
    white-space: normal;
    word-break: normal;
  }

  .split-about-char {
    display: inline-block;
    white-space: normal;
    word-break: normal;
  }

  /* Ensure parent allows wrapping to fix splittext Bug*/
  .medium-text.about-text {
    white-space: normal !important;
    overflow-wrap: break-word;
    word-wrap: break-word;
      
  }
</style>

<script>

// -----ABOUT SECTION------
  
// about text animation


// Create SplitText instance
const splitAbout = new SplitText(".medium-text.about-text", {
  type: "words,chars",
  charsClass: "split-about-char",
  wordsClass: "split-about-word"
});

// Set initial styles
gsap.set(splitAbout.chars, {
  opacity: 0,
  filter: "blur(1vw)"
});

// Animate on scroll in and reverse on scroll out (up)
ScrollTrigger.create({
  trigger: ".section-main.about-section",
  start: "top top",
  end: "bottom top",
  onEnter: () => {
    gsap.to(splitAbout.chars, {
      opacity: 1,
      filter: "blur(0vw)",
      duration: 1,
      stagger: {
        each: 0.8 / splitAbout.chars.length,
        from: "random"
      },
      ease: "power2.out"
    });
  },
  onLeaveBack: () => {
    gsap.to(splitAbout.chars, {
      opacity: 0,
      filter: "blur(1vw)",
      duration: 0.5,
      
      stagger: {
        each: 0.8 / splitAbout.chars.length,
        from: "random"
      },
      ease: "power2.in"
    });
  }
});
// END about text animation
// about image trail
  const trailEls = gsap.utils.toArray(".trail-image-wrap");
let imgIndex = 0;
let mouse = { x: 0, y: 0 };
let lastMouse = { x: 0, y: 0 };
let smoothed = { x: 0, y: 0 };
const threshold = 130;

// Capture mouse position
window.addEventListener("mousemove", (e) => {
  mouse.x = e.clientX;
  mouse.y = e.clientY;
});

// Distance check
const getDistance = (a, b) => {
  const dx = a.x - b.x;
  const dy = a.y - b.y;
  return Math.sqrt(dx * dx + dy * dy);
};

function animate() {
  smoothed.x += (mouse.x - smoothed.x) * 0.15;
  smoothed.y += (mouse.y - smoothed.y) * 0.15;

  const dist = getDistance(mouse, lastMouse);

  if (dist > threshold) {
    showNextImage(smoothed.x, smoothed.y);
    lastMouse.x = mouse.x;
    lastMouse.y = mouse.y;
  }

  requestAnimationFrame(animate);
}

function showNextImage(x, y) {
  const el = trailEls[imgIndex % trailEls.length];
  imgIndex++;

  gsap.killTweensOf(el);

  // Determine size based on viewport
  let imgSize = "15vw";
  if (window.innerWidth >= 256 && window.innerWidth <= 428) {
    imgSize = "30vw";
  }

  // Bring it to the front
  gsap.set(el, {
    x: x,
    y: y,
    width: "0vw",
    height: "0vw",
    opacity: 0,
    zIndex: imgIndex // increase with each image
  });

  // Animate in
  gsap.to(el, {
    duration: 0.4,
    ease: "power3.out",
    opacity: 1,
    width: imgSize,
    height: imgSize,
    x: x,
    y: y,
    scale: 1
  });

  // Animate out
  gsap.to(el, {
    delay: 0.75,
    duration: 0.7,
    ease: "power2.inOut",
    opacity: 0,
    width: "0vw",
    height: "0vw",
    scale: 0.3
  });
}

animate(); // Start the loop
// END about image trail

</script>

Project Section GSAP Animation

Featuring GSAP animation of revealing card of image in random angle of (-20, 20) with drag and click interaction, clicking the image will expand it and revealing project information with spiltText effect. clicking fully expanded card will return card to original size and location before dragging with new random angle.
<script>

// -----PROJECT SECTION------  
 
// DRAGGABLE Project
document.querySelectorAll('#container-draggable .drag-image').forEach((el, index) => {
	// Set initial opacity and scale using GSAP (no need for random positioning anymore)
  gsap.set(el, {
    opacity: 0,  // Start with opacity 0
    scale: 0.9,  // Start with scale 0.9
    rotation: gsap.utils.random(-20, 20)  // Optional: add random rotation for variety
  });

  // Create timeline for the appearance animation
  const appearTimeline = gsap.timeline({ paused: true });

  appearTimeline.to(el, {
    opacity: 1, // Fade in to opacity 1
    scale: 1,   // Scale up to normal size
    rotation: gsap.utils.random(-20, 20), // Random rotation for variety
    duration: 1, // Duration of the appearance animation
    ease: "back.out(1.7)" // Bounce ease for that small bounce effect
  });

  // ScrollTrigger to start the animation when the container comes into view
  ScrollTrigger.create({
    trigger: el,
    start: "top 80%",
    once: true,
    onEnter: () => {
      gsap.delayedCall(index * 0.2, () => { // Staggered start for each element
        appearTimeline.play();
      }); 

      // Re-enable Draggable after the animation starts
      Draggable.create(el, {
        bounds: "#container-draggable",
        inertia: true,
        type: "x,y",
        edgeResistance: 0.9,
        throwProps: true,
        throwResistance: 3000,
        zIndexBoost: false
      });
      
 // πŸ‘‡ Add toggle-expand interaction
let isExpanded = false; // Track expanded state
gsap.set(el, { filter: "brightness(100%)" });

el.addEventListener("click", () => {
  if (!isExpanded) {
    // Expand to full screen
    gsap.to(el, {
      width: "100vw",
      height: "100vh",
      x: "0%",
      y: "0%",
      filter: "brightness(75%)",
      top: "0%",
      left: "0%",
      bottom: "auto",
      right: "auto",
      position: "absolute",
      margin: 0,
      zIndex: 90,
      rotation: 0,
      duration: 0.6,
      ease: "power3.inOut"
    });
  } else {
    // Shrink back β€” decide size dynamically based on current viewport
    let targetWidth = "20vw";
    let targetHeight = "20vw";

    if (window.innerWidth >= 768 && window.innerWidth <= 1024) {
      targetWidth = "30vw";
      targetHeight = "30vw";
    } else if (window.innerWidth >= 480 && window.innerWidth < 720) {
      targetWidth = "35vw";
      targetHeight = "35vw";
    } else if (window.innerWidth >= 256 && window.innerWidth <= 428) {
      targetWidth = "65vw";
      targetHeight = "65vw";
    }

    gsap.to(el, {
      width: targetWidth,
      height: targetHeight,
      position: "absolute",
      filter: "brightness(100%)",
      top: "",
      left: "",
      bottom: "",
      right: "",
      margin: "auto",
      zIndex: 1,
      rotation: gsap.utils.random(-20, 20), // Random rotation each time
      duration: 0.6,
      ease: "power3.inOut"
    });
  }

  isExpanded = !isExpanded;
});
    }
  });
});
  // END DRAGGABLE  
  
  // Dragable image reveal text

 // --- Hide all target texts on load ---
document.querySelectorAll(".drag-title, .drag-detail-service, .drag-case").forEach((text) => {
  gsap.set(text, { opacity: 1 });
  text.style.visibility = "hidden";
});

// Store SplitText states
const splitTextStates = new Map();

document.querySelectorAll(".drag-image").forEach((image) => {
  image.addEventListener("click", () => {
    const card = image.closest(".drag-card");

    // Select all text elements
    const textElements = card.querySelectorAll(".drag-title, .drag-detail-service, .drag-case");
    
    // πŸ‘‡ Get the corresponding drag-link inside this card
    const dragLink = card.querySelector(".drag-link");

    textElements.forEach((el) => {
      const state = splitTextStates.get(el) || { active: false, split: null };

      if (!state.active) {
        el.style.visibility = "visible";

        const split = new SplitText(el, {
          type: "chars",
          charsClass: "split-char"
        });

        splitTextStates.set(el, { split, active: true });

        gsap.set(split.chars, {
          opacity: 0,
          filter: "blur(1vw)"
        });

        // πŸ‘‡ Stagger logic
        let staggerOptions;
        if (el.classList.contains("drag-title")) {
          // Random for title
          staggerOptions = {
            each: 1 / split.chars.length,
            from: "random"
          };
        } else {
          // Left to right for detail-service and case
          staggerOptions = 0.02;
        }
		
        gsap.to(split.chars, {
          opacity: 1,
          filter: "blur(0vw)",
          duration: 1,
          color: "#FFFFFF",
          stagger: staggerOptions,
          ease: "power2.out"
        });

        // πŸ‘‡ Show the drag-link (link block)
        if (dragLink) {
          gsap.to(dragLink, {
            opacity: 1,
            pointerEvents: "auto",
            duration: 0
          });
        }

      } else {
        gsap.to(el, {
          opacity: 0,
          duration: 0,
          onComplete: () => {
            state.split?.revert();
            splitTextStates.set(el, { active: false, split: null });
            el.style.visibility = "hidden";
            gsap.set(el, { opacity: 1 });
          }
        });

        // πŸ‘‡ Hide the drag-link again
        if (dragLink) {
          gsap.to(dragLink, {
            opacity: 0,
            pointerEvents: "none",
            duration: 0
          });
        }
      }
    });
  });
});
// END Dragable image reveal text

// LINK inside project card animation
 gsap.registerPlugin(TextPlugin);

document.querySelectorAll(".drag-case").forEach((el) => {
  const originalText = el.textContent;
  let isTyping = false; // πŸ” flag to prevent repeat typing

  el.addEventListener("mouseenter", () => {
    if (isTyping) return; // βœ… Skip if already typing

    isTyping = true;
    el.textContent = "";

    gsap.to(el, {
      duration: 1,
      text: originalText,
      ease: "none",
      onComplete: () => {
        isTyping = false; // βœ… allow replay next time
      }
    });
  });

  el.addEventListener("mouseleave", () => {
    // Optional: reset text if you want to replay each hover
    // el.textContent = originalText;
    // isTyping = false;
  });
});

//END LInk inside project animation


</script>

Service Section GSAP Animation

Featuring draggable slider using GSAP animation and GSAP Counter Text that easier to modify.
<script>

// -----SERVICE SECTION------  
 
// Service slider  
gsap.registerPlugin(Draggable);

const gallery = document.querySelector(".service-gallery-slide");

Draggable.create(gallery, {
  type: "x",
  bounds: ".service-gallery", // container element
  inertia: true
});
// END Service slider
  
// Service Counter
  gsap.registerPlugin(ScrollTrigger);

ScrollTrigger.create({
  trigger: ".service-counter",
  start: "top 80%", // starts when the top of .service-counter is 80% down the viewport
  once: true,       // run only once
  onEnter: () => {
    document.querySelectorAll(".number").forEach(counter => {
      let finalValue = parseInt(counter.textContent, 10); // read existing number
      gsap.fromTo(counter, 
        { innerText: 0 }, 
        {
          innerText: finalValue,
          duration: 2,
          ease: "power1.out",
          snap: { innerText: 1 }
        }
      );
    });
  }
});

// END Service Counter


</script>

Testimonial Section GSAP Animation

Featuring testimonial slider using GSAP animation. Using button as navigation and will bounce when start or end slider is reached.
<script>

// ----- Testimonial Section ------- 
  
// Bouncy Slider for Testimonials
document.addEventListener("DOMContentLoaded", () => {
  const track = document.querySelector(".testimonial-track");
  const slides = document.querySelectorAll(".single-testimonial");
  const prevBtn = document.querySelector(".previous-button");
  const nextBtn = document.querySelector(".next-button");

  let currentIndex = 0;
  const totalSlides = slides.length;

  function showSlide(index) {
    gsap.to(track, {
      x: `${-index * 100}%`,
      duration: 0.6,
      ease: "power2.inOut"
    });
  }

  function bounce(direction) {
    let overshoot = direction === "next" 
      ? (-currentIndex * 100 - 5) // bounce left
      : (-currentIndex * 100 + 5); // bounce right

    gsap.to(track, {
      x: `${overshoot}%`,
      duration: 0.3,
      ease: "power1.out",
      onComplete: () => {
        gsap.to(track, {
          x: `${-currentIndex * 100}%`,
          duration: 0.4,
          ease: "power2.out"
        });
      }
    });
  }

  nextBtn.addEventListener("click", () => {
    if (currentIndex < totalSlides - 1) {
      currentIndex++;
      showSlide(currentIndex);
    } else {
      bounce("next");
    }
  });

  prevBtn.addEventListener("click", () => {
    if (currentIndex > 0) {
      currentIndex--;
      showSlide(currentIndex);
    } else {
      bounce("prev");
    }
  });
});
// END Bouncy Slider for Testimonials   

</script>

Testimonial Section GSAP Animation

Featuring testimonial slider using GSAP animation. Using button as navigation and will bounce when start or end slider is reached.
<script>

// ----- Testimonial Section ------- 
  
// Bouncy Slider for Testimonials
document.addEventListener("DOMContentLoaded", () => {
  const track = document.querySelector(".testimonial-track");
  const slides = document.querySelectorAll(".single-testimonial");
  const prevBtn = document.querySelector(".previous-button");
  const nextBtn = document.querySelector(".next-button");

  let currentIndex = 0;
  const totalSlides = slides.length;

  function showSlide(index) {
    gsap.to(track, {
      x: `${-index * 100}%`,
      duration: 0.6,
      ease: "power2.inOut"
    });
  }

  function bounce(direction) {
    let overshoot = direction === "next" 
      ? (-currentIndex * 100 - 5) // bounce left
      : (-currentIndex * 100 + 5); // bounce right

    gsap.to(track, {
      x: `${overshoot}%`,
      duration: 0.3,
      ease: "power1.out",
      onComplete: () => {
        gsap.to(track, {
          x: `${-currentIndex * 100}%`,
          duration: 0.4,
          ease: "power2.out"
        });
      }
    });
  }

  nextBtn.addEventListener("click", () => {
    if (currentIndex < totalSlides - 1) {
      currentIndex++;
      showSlide(currentIndex);
    } else {
      bounce("next");
    }
  });

  prevBtn.addEventListener("click", () => {
    if (currentIndex > 0) {
      currentIndex--;
      showSlide(currentIndex);
    } else {
      bounce("prev");
    }
  });
});
// END Bouncy Slider for Testimonials   

</script>

CTAΒ Section GSAP Animation

Featuring splitText animation for some text element in CTA section(Following cursor button is not GSAP animation).
<script>

//-----CTA SECTION------
  
// CTA Title Animation
 const splitContact = new SplitText(".contact-title", { type: "chars" });

// Set initial opacity & blur
gsap.set(splitContact.chars, { 
  opacity: 0, 
  filter: "blur(1vw)"
});

gsap.to(splitContact.chars, {
  scrollTrigger: {
    trigger: ".cta-wrap",
    start: "top 25%", // when .cta-wrap center hits viewport center
    end: "bottom top",
    toggleActions: "play none none none" // play on enter, reverse on leave
  },
  opacity: 1,
  filter: "blur(0vw)",
  duration: 1,
    stagger: {
    each: 0.8 / splitContact.chars.length,
    from: "random"
  },
  ease: "power2.out"
});
// END CTA Title Animation
  
//CTA Following button text type effect
  
document.addEventListener("DOMContentLoaded", () => {
  const pointer = document.querySelector(".cursor-pointer");
  const textEl = document.querySelector(".pointer-text");
  const arrow = document.querySelector(".arrow-icon");
  const textContent = textEl.textContent;

  let typingTween;

  // Initial state
  textEl.textContent = "";

  pointer.addEventListener("mouseenter", () => {
    gsap.killTweensOf([arrow, textEl]);
    if (typingTween) typingTween.kill();

    // Fade out arrow
    gsap.to(arrow, { opacity: 0, duration: 0.2, ease: "power2.out", delay: 0.25 });

    // Typing effect
    textEl.textContent = "";
    typingTween = gsap.to({}, { 
      delay: 0.25,
      duration: textContent.length * 0.05, 
      onUpdate: function() {
        let progress = this.progress();
        let chars = Math.floor(progress * textContent.length);
        textEl.textContent = textContent.substring(0, chars);
      }
    });
  });

  pointer.addEventListener("mouseleave", () => {
    gsap.killTweensOf([arrow, textEl]);
    if (typingTween) typingTween.kill();

    // Timeline with slight offset
    const tl = gsap.timeline({ delay: 0.25 });
    tl.to(textEl, { textContent: "", duration: 0.3, ease: "power1.inOut" }, 0)
      .to(arrow, { opacity: 1, duration: 0.2, ease: "power2.inOut" }, 0.05); // arrow starts 0.05s after text fade begins
  });
});
// END Following button text type effect


</script>