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 additionnal explanation to help you understand everything.


Text animations
Add the Custom Attributes on the text/div below to activate the text animation on any element on this template.

stagger-on-scroll = line
(Site settings) Footer Code - Smooth Scroll
This GSAP integration enables ultra-smooth scrolling using the Lenis library, fully synchronized with ScrollTrigger animations. It ensures a seamless scroll experience across devices, with built-in support for momentum, easing, and touch responsiveness. The setup uses GSAP’s ticker for validation compliance and performance, keeping scroll-based animations in perfect sync. To activate, no custom attributes are needed—Lenis handles all scrolling natively once initialized.
1<!-- Lenis Smooth Scroll -->
2
3<script src="https://unpkg.com/lenis@1.3.1/dist/lenis.min.js"></script>
4
5<script>
6document.addEventListener("DOMContentLoaded", () => {
7  // Register necessary plugins (required for validation)
8  gsap.registerPlugin(ScrollTrigger);
9
10  // Initialize Lenis
11  const lenis = new Lenis({
12    duration: 1.2,
13    easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)), // expo.out
14    smooth: true,
15    smoothTouch: false,
16    touchMultiplier: 2
17  });
18
19  // Sync ScrollTrigger with Lenis
20  lenis.on("scroll", ScrollTrigger.update);
21
22  // Use GSAP's ticker to drive Lenis — this is required to pass GSAP validation
23  gsap.ticker.add((time) => {
24    lenis.raf(time * 1000);
25  });
26
27  // Optional: Fire custom event to trigger delayed animations
28  window.dispatchEvent(new CustomEvent("GSAPReady", {
29    detail: { lenis }
30  }));
31});
32</script>

(CTA) View projects - image trail animation
This script creates a mouse-following image trail effect using GSAP. When you move the cursor over a section with fc-trail-image="component", it dynamically fades and scales images in and out, leaving a trail. You can customize behavior using attributes like fc-trail-image-threshold, fc-trail-image-scale-from, and more—all within Webflow’s custom attribute panel.
1<!-- GSAP Trail Image Effect -->
2
3<style>
4  [fc-trail-image=list] img {
5    opacity: 0;
6    position: absolute;
7    will-change: transform;
8    pointer-events: none;
9    max-width: none;
10  }
11</style>
12
13<script>
14// Utility functions
15const MathUtils = {
16  lerp: (a, b, n) => (1 - n) * a + n * b,
17  distance: (x1, y1, x2, y2) => Math.hypot(x2 - x1, y2 - y1)
18};
19
20// Get mouse position relative to the trail container
21const getMousePos = (e, container) => {
22  const rect = container.getBoundingClientRect();
23  return {
24    x: e.clientX - rect.left,
25    y: e.clientY - rect.top
26  };
27};
28
29// Represents a single image in the trail
30class Image {
31  constructor(el) {
32    this.DOM = { el };
33    this.defaultStyle = {
34      scale: 1,
35      x: 0,
36      y: 0,
37      opacity: 0
38    };
39    this.getRect();
40    this.initEvents();
41  }
42
43  initEvents() {
44    window.addEventListener('resize', () => this.resize());
45  }
46
47  resize() {
48    gsap.set(this.DOM.el, this.defaultStyle);
49    this.getRect();
50  }
51
52  getRect() {
53    this.rect = this.DOM.el.getBoundingClientRect();
54  }
55
56  isActive() {
57    return gsap.isTweening(this.DOM.el) || this.DOM.el.style.opacity != 0;
58  }
59}
60
61// Controls the image trail behavior
62class ImageTrail {
63  constructor(list, mouseThreshold, opacityFrom, scaleFrom, opacityTo, scaleTo, mainDuration, mainEase, fadeOutDuration, fadeOutDelay, fadeOutEase, resetIndex, resetIndexDelay) {
64    this.DOM = { content: list };
65    this.images = [];
66    [...this.DOM.content.querySelectorAll('img')].forEach(img => this.images.push(new Image(img)));
67    this.imagesTotal = this.images.length;
68    this.imgPosition = 0;
69    this.zIndexVal = 1;
70
71    // ✅ Attribute-based values from Webflow
72    this.threshold = isNaN(mouseThreshold) ? 100 : mouseThreshold;                  // `fc-trail-image-threshold`
73    this.opacityFrom = isNaN(opacityFrom) ? 0.6 : opacityFrom;                      // `fc-trail-image-opacity-from`
74    this.scaleFrom = isNaN(scaleFrom) ? 0.8 : scaleFrom;                            // `fc-trail-image-scale-from`
75    this.opacityTo = isNaN(opacityTo) ? 1 : opacityTo;                              // `fc-trail-image-opacity-to`
76    this.scaleTo = isNaN(scaleTo) ? 1 : scaleTo;                                    // `fc-trail-image-scale-to`
77    this.mainDuration = isNaN(mainDuration) ? 0.7 : mainDuration;                  // `fc-trail-image-main-duration`
78    this.mainEase = mainEase === null ? 'power3' : mainEase;                       // `fc-trail-image-main-ease`
79    this.fadeOutDuration = isNaN(fadeOutDuration) ? 1 : fadeOutDuration;          // `fc-trail-image-fade-out-duration`
80    this.fadeOutDelay = isNaN(fadeOutDelay) ? 0.3 : fadeOutDelay;                 // `fc-trail-image-fade-out-delay`
81    this.fadeOutEase = fadeOutEase === null ? 'power3' : fadeOutEase;             // `fc-trail-image-fade-out-ease`
82    this.resetIndex = resetIndex === null ? "false" : resetIndex;                 // `fc-trail-image-reset-index`
83    this.resetIndexDelay = isNaN(resetIndexDelay) ? 200 : resetIndexDelay;        // `fc-trail-image-reset-index-delay`
84
85    this.mousePos = { x: 0, y: 0 };
86    this.lastMousePos = { x: 0, y: 0 };
87    this.cacheMousePos = { x: 0, y: 0 };
88    this.frameCount = 0;
89    this.stopAnimationFrame = false;
90  }
91
92  render() {
93    const distance = MathUtils.distance(this.mousePos.x, this.mousePos.y, this.lastMousePos.x, this.lastMousePos.y);
94    this.cacheMousePos.x = MathUtils.lerp(this.cacheMousePos.x || this.mousePos.x, this.mousePos.x, 0.1);
95    this.cacheMousePos.y = MathUtils.lerp(this.cacheMousePos.y || this.mousePos.y, this.mousePos.y, 0.1);
96
97    if (distance > this.threshold) {
98      this.showNextImage();
99      ++this.zIndexVal;
100      this.imgPosition = this.imgPosition < this.imagesTotal - 1 ? this.imgPosition + 1 : 0;
101      this.lastMousePos = this.mousePos;
102    }
103
104    let isIdle = this.images.every((img) => !img.isActive());
105
106    if (isIdle) {
107      this.frameCount++;
108      if (this.resetIndex === "true" && this.frameCount >= this.resetIndexDelay) {
109        this.frameCount = 0;
110        this.imgPosition = 0;
111      }
112      if (this.zIndexVal !== 1) {
113        this.zIndexVal = 1;
114      }
115    }
116
117    if (!this.stopAnimationFrame) requestAnimationFrame(() => this.render());
118  }
119
120  showNextImage() {
121    const img = this.images[this.imgPosition];
122    gsap.killTweensOf(img.DOM.el);
123
124    gsap.timeline()
125      .set(img.DOM.el, {
126        opacity: this.opacityFrom,
127        scale: this.scaleFrom,
128        zIndex: this.zIndexVal,
129        x: this.cacheMousePos.x - img.rect.width / 2,
130        y: this.cacheMousePos.y - img.rect.height / 2
131      })
132      .to(img.DOM.el, {
133        ease: this.mainEase,
134        x: this.mousePos.x - img.rect.width / 2,
135        y: this.mousePos.y - img.rect.height / 2,
136        opacity: this.opacityTo,
137        scale: this.scaleTo,
138        duration: this.mainDuration
139      })
140      .to(img.DOM.el, {
141        ease: this.fadeOutEase,
142        opacity: 0,
143        scale: this.scaleFrom,
144        duration: this.fadeOutDuration,
145        delay: this.fadeOutDelay,
146        onComplete: () => {
147          // Reset image style for reuse
148          gsap.set(img.DOM.el, {
149            x: 0,
150            y: 0,
151            scale: 1,
152            opacity: 0,
153            zIndex: 1
154          });
155        }
156      });
157  }
158}
159
160// Initialize after DOM is ready
161document.addEventListener("DOMContentLoaded", () => {
162  requestAnimationFrame(() => {
163    const components = document.querySelectorAll('[fc-trail-image=component]');
164    let imageTrails = [];
165
166    components.forEach((component, i) => {
167      const list = component.querySelector('[fc-trail-image=list]');
168      const mouseThreshold = parseInt(component.getAttribute('fc-trail-image-threshold'));
169      const opacityFrom = parseFloat(component.getAttribute('fc-trail-image-opacity-from'));
170      const scaleFrom = parseFloat(component.getAttribute('fc-trail-image-scale-from'));
171      const opacityTo = parseFloat(component.getAttribute('fc-trail-image-opacity-to'));
172      const scaleTo = parseFloat(component.getAttribute('fc-trail-image-scale-to'));
173      const mainDuration = parseFloat(component.getAttribute('fc-trail-image-main-duration'));
174      const mainEase = component.getAttribute('fc-trail-image-main-ease');
175      const fadeOutDuration = parseFloat(component.getAttribute('fc-trail-image-fade-out-duration'));
176      const fadeOutDelay = parseFloat(component.getAttribute('fc-trail-image-fade-out-delay'));
177      const fadeOutEase = component.getAttribute('fc-trail-image-fade-out-ease');
178      const resetIndex = component.getAttribute('fc-trail-image-reset-index');
179      const resetIndexDelay = parseInt(component.getAttribute('fc-trail-image-reset-index-delay'));
180
181      // Track mouse inside component
182      component.addEventListener('mousemove', function (ev) {
183        if (imageTrails[i].resetIndex === "true" && imageTrails[i].frameCount > 0)
184          imageTrails[i].frameCount = 0;
185        imageTrails[i].mousePos = getMousePos(ev, component);
186      });
187
188      // Start animation on hover
189      component.addEventListener("mouseenter", () => {
190        imageTrails[i].stopAnimationFrame = false;
191        requestAnimationFrame(() => imageTrails[i].render());
192
193        if (imageTrails[i].resetIndex === "true") {
194          imageTrails[i].imgPosition = 0;
195          imageTrails[i].frameCount = 0;
196        }
197      });
198
199      // Stop animation on mouse leave
200      component.addEventListener("mouseleave", () => {
201        imageTrails[i].stopAnimationFrame = true;
202      });
203
204      // Create instance of ImageTrail
205      imageTrails.push(new ImageTrail(
206        list,
207        mouseThreshold,
208        opacityFrom,
209        scaleFrom,
210        opacityTo,
211        scaleTo,
212        mainDuration,
213        mainEase,
214        fadeOutDuration,
215        fadeOutDelay,
216        fadeOutEase,
217        resetIndex,
218        resetIndexDelay
219      ));
220    });
221  });
222});
223</script>

Attributes

fc-trail-image="component"

*Required

Defines the entire interactive image trail wrapper

fc-trail-image="list"

*Required

The wrapper that contains all the <img> elements to animate

fc-trail-image-threshold

Optional

Minimum mouse movement (in px) before next image is triggered

fc-trail-image-opacity-from

Optional

Starting opacity value for each image (e.g. 0.6)

fc-trail-image-scale-from

Optional

Starting scale value for each image (e.g. 0.8)

fc-trail-image-opacity-to

Optional

Final opacity value when image appears (e.g. 1)

fc-trail-image-scale-to

Optional

Final scale value when image appears (e.g. 1)

fc-trail-image-main-duration

Optional

Duration (in seconds) of the image entering animation

fc-trail-image-main-ease

Optional

GSAP easing function (e.g. power3, sine.inOut)

fc-trail-image-fade-out-duration

Optional

How long the image takes to fade out

fc-trail-image-fade-out-delay

Optional

How long to wait before fading out

fc-trail-image-fade-out-ease

Optional

GSAP easing function for the fade out

fc-trail-image-reset-index

Optional

"true" resets image index when idle or on mouse enter

fc-trail-image-reset-index-delay

Optional

Number of frames before reset happens if reset-index is "true"


*To delete animations, you only need to delete the code block linked to the animation. Every code block is identified.