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.
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.
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
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.
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.
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>
45Change 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.
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.
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.
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.
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>
34Change 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.
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.
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.
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.
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.