Instructions

This template uses GSAP for sliders, marquees, scroll-based movement, stacking cards, horizontal scrolling, parallax images, and SVG drawing effects.

! Before editing the code, duplicate the template or save a backup version. After making changes, save the project and republish the site.

How to edit site-wide GSAP Animations

Some GSAP animations are added in the global site custom code, while others are added only to specific pages.

To find gsap animations open the Webflow project → Go to Site settings → Open the Custom code tab → Footer code areas. After editing, click Save changes. Publish the site to see the changes live.

1. Smooth Scroll + Parallax Image Animation

This script adds smooth scrolling with Lenis and moves images vertically on scroll using GSAP ScrollTrigger. adds smooth scrolling and a vertical parallax effect to images.

1<!-- Smooth scroll + Parallax image animation -->
2<script>
3window.addEventListener("load", function () {
4  if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return;
5
6  gsap.registerPlugin(ScrollTrigger);
7
8  const isDesktop = window.matchMedia("(min-width: 992px)").matches;
9
10  if (isDesktop) {
11    const lenisScript = document.createElement("script");
12    lenisScript.src = "https://unpkg.com/lenis@1.3.21/dist/lenis.min.js";
13
14    lenisScript.onload = function () {
15      if (typeof Lenis === "undefined") return;
16
17      const lenis = new Lenis({
18        lerp: 0.07,
19        smoothWheel: true,
20        wheelMultiplier: 0.8,
21        autoRaf: false
22      });
23
24      lenis.on("scroll", ScrollTrigger.update);
25
26      gsap.ticker.add((time) => {
27        lenis.raf(time * 1000);
28      });
29
30      gsap.ticker.lagSmoothing(0);
31    };
32
33    document.body.appendChild(lenisScript);
34  }
35
36  gsap.utils.toArray(".parallax-image").forEach((img) => {
37    const wrap = img.closest(".parallax-wrap");
38    if (!wrap) return;
39
40    gsap.to(img, {
41      yPercent: 30,
42      ease: "none",
43      force3D: true,
44      scrollTrigger: {
45        trigger: wrap,
46        start: "top bottom",
47        end: "bottom top",
48        scrub: isDesktop ? true : 1,
49        invalidateOnRefresh: true
50      }
51    });
52  });
53
54  ScrollTrigger.refresh();
55});
56</script>

The yPercent value controls how far the image moves vertically during scroll.

When changing yPercent, also update the .parallax-image height and top position so the image has enough extra space to move without showing empty gaps.
Use this rule:
Image height = 100% + yPercent value
Top position = same value as yPercent

Examples:
yPercent: 30
Image height: 130%
Top position: 30%

yPercent: 20
Image height: 120%
Top position: 20%

To remove

Delete the Smooth scroll + Parallax image animation script. The images will stop moving on scroll. Also reset the .parallax-image styles in Webflow: Height: 100% or Auto, Position: Static

2. Hero Marquee Strip

This script creates a simple infinite horizontal marquee in the hero section.

1<!-- Hero Marquee Strip -->
2<script>
3  window.Webflow ||= [];
4  window.Webflow.push(() => {
5    gsap.to(".marque", {
6      xPercent: -50,
7      duration: 20,
8      ease: "none",
9      repeat: -1
10    });
11  });
12</script>
  • Change duration: 20 to control speed.

  • Change filter: 'blur(4px)' to control blur amount.

To remove

Delete the Hero Marquee Strip script. The hero marquee content will remain visible but will no longer move.

3. Count Up on scroll (used in Home V1)

Use this version when the numbers are lower on the page and should animate only when the user reaches them.

1<!-- Countup Animation on scroll -->
2<script>
3  window.Webflow ||= [];
4  window.Webflow.push(() => {
5    if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return;
6
7    gsap.registerPlugin(ScrollTrigger);
8
9    gsap.utils.toArray(".count-on-scroll").forEach((counter) => {
10      const original = counter.textContent.trim();
11      const match = original.match(/[-+]?(?:\d+\.?\d*|\.\d+)/);
12
13      if (!match) return;
14
15      const number = match[0];
16      const endValue = Number(number);
17
18      if (!Number.isFinite(endValue)) return;
19
20      const decimals = number.includes(".") ? number.split(".")[1].length : 0;
21      const prefix = original.slice(0, match.index);
22      const suffix = original.slice(match.index + number.length);
23      const state = { value: 0 };
24
25      gsap.fromTo(
26        state,
27        { value: 0 },
28        {
29          value: endValue,
30          duration: 1.6,
31          ease: "power2.out",
32          scrollTrigger: {
33            trigger: counter,
34            start: "top 85%",
35            once: true
36          },
37          onStart: () => {
38            counter.textContent = `${prefix}${(0).toFixed(decimals)}${suffix}`;
39          },
40          onUpdate: () => {
41            const value = decimals
42              ? state.value.toFixed(decimals)
43              : Math.round(state.value);
44
45            counter.textContent = `${prefix}${value}${suffix}`;
46          },
47          onComplete: () => {
48            counter.textContent = original;
49          }
50        }
51      );
52    });
53  });
54</script>
  • Change the number directly in the Webflow text element.

  • Add .count-on-scroll subclass to any number you want to animate.

  • Remove .countup from any number you want to keep static.

4. Count Up on load (used in Home V2)

Use this version when the numbers are visible near the top of the page and should animate immediately.

1<!-- Countup Animation on load -->
2<script>
3  window.Webflow ||= [];
4  window.Webflow.push(() => {
5    if (typeof gsap === "undefined") return;
6
7    gsap.utils.toArray(".count-on-load").forEach((counter) => {
8      const original = counter.textContent.trim();
9      const match = original.match(/[-+]?(?:\d+\.?\d*|\.\d+)/);
10
11      if (!match) return;
12
13      const number = match[0];
14      const endValue = Number(number);
15
16      if (!Number.isFinite(endValue)) return;
17
18      const decimals = number.includes(".") ? number.split(".")[1].length : 0;
19      const prefix = original.slice(0, match.index);
20      const suffix = original.slice(match.index + number.length);
21      const state = { value: 0 };
22      const zero = decimals ? (0).toFixed(decimals) : "0";
23
24      counter.textContent = `${prefix}${zero}${suffix}`;
25
26      gsap.to(state, {
27        value: endValue,
28        duration: gsap.utils.clamp(1.1, 2.6, Math.abs(endValue) / 120),
29        delay: 0.5,
30        ease: "sine.inOut",
31        onUpdate: () => {
32          const value = decimals
33            ? state.value.toFixed(decimals)
34            : Math.round(state.value);
35
36          counter.textContent = `${prefix}${value}${suffix}`;
37        },
38        onComplete: () => {
39          counter.textContent = original;
40        }
41      });
42    });
43  });
44</script>
45
  • Change the number directly in the Webflow text element.

  • Add .count-on-load subclass to any number you want to animate.

  • Remove .countup from any number you want to keep static.

5. Service V1 Stacking Cards

This script creates a stacking effect for service cards. As the next service card scrolls in, the previous card scales down and its content fades out.

1<!-- Service V1 Stacking Cards  -->
2<script>
3  window.Webflow ||= [];
4  window.Webflow.push(() => {
5    if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return;
6
7    gsap.registerPlugin(ScrollTrigger);
8
9    const cards = gsap.utils.toArray(".service-cards-v1");
10    if (cards.length < 2) return;
11
12    cards.forEach((card, index) => {
13      if (index === 0) return;
14
15      const previousCard = cards[index - 1];
16      const previousContent = previousCard.querySelector(".service-content");
17
18      const timeline = gsap.timeline({
19        scrollTrigger: {
20          trigger: card,
21          start: "top 80%",
22          end: "top 30%",
23          scrub: 1
24        }
25      });
26
27      timeline.to(
28        previousCard,
29        {
30          scale: 0.75,
31          transformOrigin: "center top",
32          ease: "none"
33        },
34        0
35      );
36
37      if (previousContent) {
38        timeline.to(
39          previousContent,
40          {
41            opacity: 0,
42            ease: "none"
43          },
44          0
45        );
46      }
47    });
48  });
49</script>
  • Change scale: 0.75 to control how small the previous card becomes.

  • Change opacity: 0 to control how much the content fades.

To remove

Delete the Service V1 Stacking Cards script. The service cards will remain just sticky while scrolling.

6. Team Horizontal Scroll

This script creates a stacking effect for service cards. As the next service card scrolls in, the previous card scales down and its content fades out.

1<!-- Team Horizontal Scroll -->
2<script>
3  window.Webflow ||= [];
4  window.Webflow.push(() => {
5    if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return;
6
7    gsap.registerPlugin(ScrollTrigger);
8
9    gsap.utils.toArray(".h-scroll-wrapper").forEach((scrollWrapper) => {
10      const track = scrollWrapper.querySelector(".horizontal-track");
11      const pinSection = scrollWrapper.closest(".team-wrapper-v2");
12
13      if (!track || !pinSection) return;
14
15      const getScrollAmount = () => {
16        return Math.max(track.scrollWidth - scrollWrapper.clientWidth, 0);
17      };
18
19      gsap.to(track, {
20        x: () => -getScrollAmount(),
21        ease: "none",
22        force3D: true,
23        scrollTrigger: {
24          trigger: pinSection,
25          start: "top top",
26          end: () => `+=${getScrollAmount()}`,
27          pin: true,
28          pinSpacing: true,
29          scrub: 1,
30          invalidateOnRefresh: true,
31          anticipatePin: 1
32        }
33      });
34    });
35
36    window.addEventListener("load", () => {
37      ScrollTrigger.refresh();
38    });
39  });
40</script>

To remove

Delete the Team Horizontal Scroll script. The team section will no longer pin or move horizontally on scroll.

7. Blog V2 GSAP Slider

This script turns blog cards (Blog V2) into a responsive slider with previous and next buttons.

1<!-- Blog V2 CMS Slider -->
2<script>
3  window.Webflow ||= [];
4  window.Webflow.push(() => {
5    if (typeof gsap === "undefined") return;
6
7    gsap.utils.toArray(".blog-slider").forEach((slider) => {
8      const mask = slider.querySelector(".slider-mask");
9      const track = slider.querySelector(".slider-track");
10      const slides = gsap.utils.toArray(slider.querySelectorAll(".slide-item"));
11      const prev = slider.querySelector(".slider-prev");
12      const next = slider.querySelector(".slider-next");
13
14      if (!mask || !track || slides.length < 2) return;
15
16      let index = 0;
17
18      const getGap = () => {
19        const style = window.getComputedStyle(track);
20        return parseFloat(style.columnGap || style.gap || 0) || 0;
21      };
22
23      const getVisibleCount = () => {
24        const slideWidth = slides[0].offsetWidth;
25        const gap = getGap();
26
27        if (!slideWidth) return 1;
28
29        return Math.max(
30          1,
31          Math.floor((mask.offsetWidth + gap) / (slideWidth + gap))
32        );
33      };
34
35      const getMaxIndex = () => {
36        return Math.max(0, slides.length - getVisibleCount());
37      };
38
39      const getSlideX = () => {
40        return slides[index] ? -slides[index].offsetLeft : 0;
41      };
42
43      const goTo = (nextIndex, animate = true) => {
44        const max = getMaxIndex();
45
46        if (nextIndex > max) {
47          index = 0;
48        } else if (nextIndex < 0) {
49          index = max;
50        } else {
51          index = nextIndex;
52        }
53
54        gsap.to(track, {
55          x: getSlideX(),
56          duration: animate ? 0.5 : 0,
57          ease: "sine.inOut",
58          overwrite: "auto",
59          force3D: true
60        });
61      };
62
63      prev?.addEventListener("click", () => {
64        goTo(index - 1);
65      });
66
67      next?.addEventListener("click", () => {
68        goTo(index + 1);
69      });
70
71      window.addEventListener("resize", () => {
72        goTo(index, false);
73      });
74
75      goTo(0, false);
76    });
77  });
78</script>
  • Change gap = 20 to adjust spacing between slides.

  • Change swipeDistance = 50 to make swipe gestures more or less sensitive.

  • Change duration: 0.5 to adjust slider transition speed.

  • Edit slidesPerView() to change how many slides show on desktop, tablet, and mobile.

8. Blog V3 Stacking Cards

This script creates a stacking scroll effect. As a new blog card enters, the previous card scales down and becomes blurred.

1<!-- Blog V3 Stacking Cards -->
2<script>
3document.addEventListener('DOMContentLoaded', () => {
4  if (typeof gsap === 'undefined' || typeof ScrollTrigger === 'undefined') {
5    console.warn('GSAP or ScrollTrigger is not loaded.');
6    return;
7  }
8
9  gsap.registerPlugin(ScrollTrigger);
10
11  const cards = document.querySelectorAll('.stacking-card');
12  if (!cards.length) return;
13
14  cards.forEach(card => {
15    const previousCard = card.previousElementSibling;
16
17    if (!previousCard || !previousCard.classList.contains('stacking-card')) return;
18
19    gsap.to(previousCard, {
20      scale: 0.80,
21      filter: 'blur(4px)',
22      transformOrigin: 'center top',
23      ease: 'none',
24      scrollTrigger: {
25        trigger: card,
26        start: 'top 50%',
27        end: 'top top',
28        scrub: 1
29      }
30    });
31  });
32});
33</script>
34
  • Change scale: 0.80 to control how small the previous card becomes.

  • Change filter: 'blur(4px)' to control blur amount.

To remove

Delete the Blog V3 Stacking Cards script. The cards will stay static while scrolling.

9. Draw SVG Arrows V3

This script creates a stacking effect for service cards. As the next service card scrolls in, the previous card scales down and its content fades out.

1<!-- Draw SVG Arrows V3 -->
2<script>
3gsap.registerPlugin(DrawSVGPlugin, ScrollTrigger);
4
5gsap.set("#arrow-1 path, #arrow-2 path, #arrow-3 path", { drawSVG: 0 });
6
7function makeArrowTl(id) {
8  const tl = gsap.timeline({ paused: true });
9  tl
10    .to(`#${id}-path`, {
11      drawSVG: "100%",
12      duration: 1,
13      ease: "sine.inOut"
14    })
15    .to(`#${id}-head`, {
16      drawSVG: "100%",
17      duration: 0.4,
18      ease: "sine.inOut"
19    }, "-=0.2");
20
21  ScrollTrigger.create({
22    trigger: `#${id}`,
23    start: "top 80%",
24    onEnter: () => tl.play()
25  });
26}
27
28makeArrowTl("arrow-1");
29makeArrowTl("arrow-2");
30makeArrowTl("arrow-3");
31</script>
  • Change duration: 1 to control the arrow line drawing speed.

  • Change duration: 0.4 to control the arrow head drawing speed.

To remove

Delete the Draw SVG Arrows V3 script. The SVG arrows will appear without the drawing animation.

10. Testimonials Marquee

This script creates an infinite scrolling testimonial marquee. It duplicates the testimonial items automatically to create a seamless loop.

1<!-- Testimonials Marquee V3 -->
2<script>
3  window.Webflow ||= [];
4  window.Webflow.push(() => {
5    if (typeof gsap === "undefined" || typeof ScrollTrigger === "undefined") return;
6
7    gsap.registerPlugin(ScrollTrigger);
8
9    gsap.matchMedia().add("(prefers-reduced-motion: no-preference)", () => {
10      gsap.utils.toArray('[gsap-marquee="testimonials"]').forEach((marquee) => {
11        const tween = gsap.to(marquee, {
12          xPercent: -50,
13          duration: 16,
14          ease: "none",
15          repeat: -1,
16          force3D: true
17        });
18
19        ScrollTrigger.observe({
20          target: marquee,
21          type: "pointer,touch",
22          onHover: () => tween.pause(),
23          onHoverEnd: () => tween.play(),
24          onPress: () => tween.pause(),
25          onRelease: () => tween.play()
26        });
27      });
28    });
29  });
30</script>
  • Change duration: 16 to control marquee speed.

  • Change filter: 'blur(4px)' to control blur amount.

To remove

Delete the Testimonials Marquee script and remove the gsap-marquee="testimonials" attribute if it is no longer needed. The testimonials will stop moving and remain static.

Webflow GSAP interactions notes

H1 Animation

Most H1 headings use the .H1 class and are animated with native Webflow GSAP interactions.

The H1 headings on the Home pages use different class names because they are animated as part of the full hero section animation. This is intentional.

If you duplicate or create a new regular page H1 and want it to use the same default heading animation, add the .h1 class. If you duplicate or edit a Home page hero heading, keep its existing Home-specific class so the hero animation continues to work correctly.

SVG Icon Color

This template has some SVG icons. To change the icon color select the icon → Change the Text color in the Style panel. The SVG icon color will update automatically.