Stagger card reveal
This animation reveals cards one by one using a staggered fade-up effect.
The animation targets elements with the attribute data="bottom" and separates cards into two groups:
- Cards already visible in the viewport animate immediately on page load.
- Remaining cards animate as they enter the viewport while scrolling.
Each card transitions from lower opacity and a downward Y offset to its original position, creating a smooth layered reveal effect.
Customization options
- Use the attribute
data="bottom"to apply the animation. - Adjust the stagger value to control the delay between cards.
- Default:
0.3 - Lower value → faster sequence
- Higher value → more dramatic reveal
- Default:
- Customize the Y movement distance.
- Default:
80 - Smaller values create subtler motion.
- Default:
- Modify the animation duration.
- Default:
0.8s
- Default:
- Change the easing style for different motion feels.
power2.out→ smooth and modernpower3.out→ softer finish
- Adjust the ScrollTrigger start position.
- Default:
"top 85%" - Earlier trigger:
"top 95%" - Later trigger:
"top 70%"
- Default:
Features
- Smooth staggered card entrance animation
- Instantly animates visible cards on page load
- Reveals additional cards on scroll
- Creates depth and visual rhythm in layouts
- Fully customizable timing, spacing, and movement
- Lightweight and performance-friendly for large grids
1gsap.registerPlugin(ScrollTrigger);
2
3const cards = gsap.utils.toArray('[data="bottom"]');
4
5// Helper: check if element is already in viewport
6function isInViewport(el) {
7 const rect = el.getBoundingClientRect();
8 return rect.top < window.innerHeight && rect.bottom > 0;
9}
10
11// Split cards
12const visibleCards = cards.filter(isInViewport);
13const hiddenCards = cards.filter(card => !isInViewport(card));
14
15
16// ✅ 1. Animate visible cards on load
17gsap.from(visibleCards, {
18 opacity: 0,
19 y: 80,
20 duration: 0.8,
21 ease: "power2.out",
22 stagger: 0.3
23});
24
25
26// ✅ 2. Animate remaining cards on scroll
27hiddenCards.forEach((card, i) => {
28 gsap.from(card, {
29 opacity: 0,
30 y: 80,
31 duration: 0.8,
32 ease: "power2.out",
33 delay: i * 0.1,
34 scrollTrigger: {
35 trigger: card,
36 start: "top 85%",
37 toggleActions: "play none none none"
38 }
39 });
40});
41Counter animation
This animation creates a counting effect by animating numbers from 0 to a specified target value when the element enters the viewport.
The animation targets elements with the attribute data-counter. Each element reads its target number directly from the attribute value and smoothly increments the number using GSAP.
The animation is triggered using ScrollTrigger when the element reaches 85% of the viewport.
Customization options
- Use the attribute
data-counterto define the target number.- Example:
data-counter="150"
- Example:
- Adjust the animation duration to control counting speed.
- Default:
2s
- Default:
- Customize the easing for different motion styles.
power1.out→ smooth decelerationpower2.out→ softer finish
- Change the ScrollTrigger start point.
- Default:
"top 85%" - Earlier trigger:
"top 95%" - Later trigger:
"top 60%"
- Default:
- Use
snapto create whole-number counting instead of decimals. - Add prefixes or suffixes using additional text elements.
- Example:
+150 - Example:
150K
- Example:
Features
- Smooth animated number counting effect
- Triggered on scroll using ScrollTrigger
- Automatically reads values from attributes
- Supports customizable speed and trigger position
- Lightweight and performance-friendly
- Perfect for stats, achievements, and metrics sections
1<script>
2 gsap.registerPlugin(ScrollTrigger);
3
4 const counters = document.querySelectorAll("[data-counter]");
5
6 counters.forEach((counter) => {
7
8 // Original value from attribute
9 const value = counter.getAttribute("data-counter");
10
11 // Extract numeric part
12 const target = parseFloat(value);
13
14 // Extract suffix (%, +, K+, etc.)
15 const suffix = value.replace(/[0-9.]/g, "");
16
17 // Detect decimal places from original value
18 const decimals = (value.split(".")[1] || "").match(/^\d+/)?.[0].length || 0;
19
20 const obj = { val: 0 };
21
22 gsap.to(obj, {
23 val: target,
24 duration: 2,
25 ease: "power1.out",
26
27 scrollTrigger: {
28 trigger: counter,
29 start: "top 85%",
30 toggleActions: "play none none none"
31 },
32
33 onUpdate: () => {
34 let displayValue;
35
36 if (decimals > 0) {
37 const factor = Math.pow(10, decimals);
38 displayValue = Math.floor(obj.val * factor) / factor;
39 displayValue = displayValue.toFixed(decimals);
40 } else {
41 displayValue = Math.floor(obj.val);
42 }
43
44 counter.innerText = displayValue + suffix;
45 },
46
47 onComplete: () => {
48 counter.innerText = value;
49 }
50 });
51
52 });
53</script>
54Infinite loop carousel
This script powers an infinite sliding carousel using GSAP by cloning original items and seamlessly looping positions when reaching either boundary.
The animation targets elements with custom data-carousel attributes and sets up smooth interactive navigation:
- Cloning: Duplicates card elements into the track so sliding forward or backward never exposes empty space.
- Step Calculation: Automatically measures card width plus CSS gaps (
gaporgrid-column-gap) for accurate distance calculations. - Boundary Reset: Instantly resets position without animation (
gsap.set) when reaching the end or beginning to maintain a seamless infinite loop.
Customization options
- Target data attributes:
- Component wrapper:
data-carousel="component" - Sliding track:
data-carousel="track" - Item cards:
data-carousel="card" - Navigation buttons:
data-carousel="btn-prev"anddata-carousel="btn-next"
- Component wrapper:
- Modify transition speed:
- Default:
duration: 0.6 - Lower value $\rightarrow$ faster slide
- Higher value $\rightarrow$ slower transition
- Default:
- Change easing style:
power2.out$\rightarrow$ smooth, responsive feelpower3.out$\rightarrow$ stronger decelerationexpo.out$\rightarrow$ fast start with gradual glide
- Responsiveness:
- Automatic resize event listener recalibrates track offset instantly on window resize.
Features
- Seamless infinite looping: Cloned set guarantees no layout gaps during transitions.
- Dynamic Gap Detection: Automatically detects flexbox/grid layout gaps directly from CSS.
- Animation Guard: Uses
isAnimatingflag to prevent rapid button-click glitcheing. - Resize Friendly: Recalculates card dimensions dynamically to prevent layout breaks on viewport changes.
- Native Webflow Ready: Wrapped safely inside Webflow's JavaScript execution queue.
1<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
2
3<script>
4window.Webflow ||= [];
5window.Webflow.push(() => {
6 "use strict";
7
8 const carousels = document.querySelectorAll('[data-carousel="component"]');
9 if (!carousels.length) return;
10
11 carousels.forEach((carousel) => {
12 const track = carousel.querySelector('[data-carousel="track"]');
13 const prevBtn = carousel.querySelector('[data-carousel="btn-prev"]');
14 const nextBtn = carousel.querySelector('[data-carousel="btn-next"]');
15 let originalCards = [...carousel.querySelectorAll('[data-carousel="card"]')];
16
17 if (!track || !originalCards.length || !prevBtn || !nextBtn) return;
18
19 const totalOriginals = originalCards.length;
20
21 // Clone set twice to ensure no viewport gaps occur during infinite looping
22 originalCards.forEach((card) => {
23 const clone = card.cloneNode(true);
24 clone.setAttribute("data-carousel-clone", "true");
25 track.appendChild(clone);
26 });
27
28 const allCards = [...track.querySelectorAll('[data-carousel="card"]')];
29 let currentIndex = 0;
30 let isAnimating = false;
31
32 // Accurate calculation including card width and flex/grid track gaps
33 const getStepWidth = () => {
34 const cardWidth = originalCards[0].offsetWidth;
35 const trackStyle = window.getComputedStyle(track);
36 const gap = parseFloat(trackStyle.gap) || parseFloat(trackStyle.gridColumnGap) || 0;
37 return cardWidth + gap;
38 };
39
40 const moveSlider = (direction) => {
41 if (isAnimating) return;
42 isAnimating = true;
43
44 currentIndex += direction;
45 const stepWidth = getStepWidth();
46
47 gsap.to(track, {
48 x: -(currentIndex * stepWidth),
49 duration: 0.6,
50 ease: "power2.out",
51 onComplete: () => {
52 // Loop Forward Reset
53 if (currentIndex >= totalOriginals) {
54 currentIndex = 0;
55 gsap.set(track, { x: 0 });
56 }
57
58 // Loop Backward Reset
59 if (currentIndex < 0) {
60 currentIndex = totalOriginals - 1;
61 gsap.set(track, { x: -(currentIndex * stepWidth) });
62 }
63
64 isAnimating = false;
65 }
66 });
67 };
68
69 nextBtn.addEventListener("click", (e) => {
70 e.preventDefault();
71 moveSlider(1);
72 });
73
74 prevBtn.addEventListener("click", (e) => {
75 e.preventDefault();
76 moveSlider(-1);
77 });
78
79 window.addEventListener("resize", () => {
80 gsap.set(track, {
81 x: -(currentIndex * getStepWidth())
82 });
83 });
84 });
85});
86</script>
87Image reveal with zoom effect
This script creates a high-end image reveal effect using GSAP and ScrollTrigger by sliding away an overlay while simultaneously zooming out the underlying image.
The animation targets elements with custom data="image" attributes and executes a synchronized timeline on scroll:
- Pre-set position: Ensures the overlay covers the image (
y: "0%") before coming into view to prevent visual glitches. - Overlay reveal: Slides the overlay element downward (
y: "100%") to reveal the content beneath. - Image zoom-out: Simultaneously transitions the image from an enlarged scale (
scale: 1.3) back to its natural size (scale: 1). - One-time trigger: Runs the reveal sequence once (
once: true) as soon as the image container enters the viewport.
Customization options
- Target data attributes:
- Image element:
data="image" - Overlay element:
data="overlay"
- Image element:
- Adjust overlay transition:
- Default duration:
0.75s - Default ease:
power2.inOut$\rightarrow$ smooth acceleration and deceleration
- Default duration:
- Customize scale effect:
- Default start scale:
1.3(130%) - Lower scale:
1.1$\rightarrow$ subtle zoom - Higher scale:
1.5$\rightarrow$ dramatic cinematic reveal
- Default start scale:
- Modify animation duration:
- Default zoom duration:
1.5s - Default ease:
power1.out$\rightarrow$ smooth slowing finish
- Default zoom duration:
- Adjust ScrollTrigger start position:
- Default:
"top 85%" - Earlier trigger:
"top 95%" - Later trigger:
"top 70%"
- Default:
Features
- Synchronized timeline: Combines overlay sliding and image scaling into a seamless simultaneous motion.
- Performance optimized: Uses
force3D: trueto trigger GPU hardware acceleration for smooth rendering. - Tween safety: Kills active tweens via
gsap.killTweensOfbefore animating to prevent conflicting animations. - Single-execution reveal: Sets
once: trueso the entrance effect plays cleanly a single time without re-triggering. - DOM Ready wrapper: Runs safely inside
DOMContentLoadedto ensure elements exist before binding events.
1<script>
2document.addEventListener("DOMContentLoaded", () => {
3 gsap.registerPlugin(ScrollTrigger);
4
5 document.querySelectorAll('[data="image"]').forEach((image) => {
6
7 const wrapper = image.parentElement;
8 const overlay = wrapper.querySelector('[data="overlay"]');
9
10 // Prepare overlay before user sees it
11 gsap.set(overlay, {
12 y: "0%"
13 });
14
15 ScrollTrigger.create({
16 trigger: wrapper,
17 start: "top 85%",
18 once: true,
19
20 onEnter: () => {
21
22 gsap.killTweensOf([overlay, image]);
23
24 const tl = gsap.timeline();
25
26 // Overlay reveal
27 tl.to(overlay, {
28 y: "100%",
29 duration: 0.75,
30 ease: "power2.inOut"
31 });
32
33 // Smooth image zoom
34 tl.fromTo(image,
35 {
36 scale: 1.3
37 },
38 {
39 scale: 1,
40 duration: 1.5,
41 ease: "power1.out",
42 force3D: true
43 },
44 0
45 );
46
47 }
48 });
49
50 });
51});
52</script>Staggered image zoom reveal
This script applies a smooth scale-down and fade-in animation to images as they enter the viewport using GSAP and ScrollTrigger.
The animation targets elements with the attribute data-anim="image" and handles the transition in two phases:
- Initial State: Hides the images (
opacity: 0) and scales them up slightly (scale: 1.2) before they are revealed. - Scroll Entrance: As each image scrolls into view, it smoothly fades in to full opacity (
opacity: 1) while zooming out to its original size (scale: 1). - Staggered Delay: Adds an incremental delay based on the index (
i * 0.05) to create a subtle sequential reveal when multiple images enter the viewport together.
Customization options
- Target data attribute:
- Image element:
data-anim="image"
- Image element:
- Adjust initial image state:
- Default scale:
1.2(120%) - Lower scale:
1.05$\rightarrow$ subtle zoom effect - Higher scale:
1.4$\rightarrow$ dramatic entrance
- Default scale:
- Modify animation duration & delay:
- Default duration:
1s - Default delay stagger:
i * 0.05 - Higher stagger multiplier:
i * 0.15$\rightarrow$ more pronounced sequence
- Default duration:
- Change easing style:
power3.out$\rightarrow$ soft, smooth deceleration finishpower2.out$\rightarrow$ standard modern easeback.out(1.7)$\rightarrow$ subtle pop-in effect
- Adjust ScrollTrigger start position:
- Default:
"top 85%" - Earlier trigger:
"top 95%" - Later trigger:
"top 70%"
- Default:
Features
- Subtle staggered entrance: Sequential delay prevents all simultaneous images from triggering abruptly together.
- Clean single play action:
toggleActions: "play none none none"ensures the animation plays once cleanly without resetting on scroll up. - Smooth visual depth: Combining opacity fades with scale-down motion gives layout images an elevated cinematic feel.
- Lightweight & fast: Direct GSAP tweens provide optimal performance across complex grid layouts.
1<script>
2 // =========================
3 // IMAGE ANIMATION
4 // =========================
5
6 const images = gsap.utils.toArray('[data-anim="image"]');
7
8 gsap.set(images, {
9 opacity: 0,
10 scale: 1.2
11 });
12
13 images.forEach((image, i) => {
14
15 gsap.to(image, {
16
17 opacity: 1,
18 scale: 1,
19
20 duration: 1,
21 ease: "power3.out",
22
23 delay: i * 0.05,
24
25 scrollTrigger: {
26 trigger: image,
27 start: "top 85%",
28 toggleActions: "play none none none"
29 }
30
31 });
32
33 });
34
35</script>Split word reveal with Lenis smooth scroll
This script integrates Lenis smooth scrolling with GSAP ScrollTrigger and builds a custom split-word typography mask reveal animation.
The script targets elements with data-anim="split-words" and processes them through two core systems:
- Lenis Smooth Scroll Integration: Connects Lenis scroll updates directly to GSAP's
ScrollTrigger.updateloop for frame-perfect scroll synchronizations. - Custom Text Splitting Engine: Parses typography node trees to wrap individual words into double-layered
<span>elements:- Outer Wrapper: Applies
overflow: hiddento mask the word while using custom padding fixes to prevent descender letters (g,y,p,q) and italic slants from getting cut off. - Inner Wrapper: Handles vertical transformations (
yPercent) driven by GSAP.
- Outer Wrapper: Applies
- Scroll-Triggered Reveal: Shifts all split words down by
110%initially, then smoothly animates them up into view (yPercent: 0) in a cascading stagger sequence when scrolled into view.
Customization options
- Target data attribute:
- Animated element:
data-anim="split-words"
- Animated element:
- Modify word animation speed & stagger:
- Default duration:
1s - Default stagger:
0.045sbetween words - Faster stagger:
0.02s$\rightarrow$ snappy, rapid line text reveal - Slower stagger:
0.08s$\rightarrow$ pronounced word-by-word sequence
- Default duration:
- Change easing style:
power4.out$\rightarrow$ strong initial burst with smooth decelerationpower2.out$\rightarrow$ standard subtle transitionback.out(1.4)$\rightarrow$ playful bounce-in effect
- Adjust ScrollTrigger start position:
- Default:
"top 92%" - Earlier trigger:
"top 98%" - Later trigger:
"top 75%"
- Default:
Features
- Lenis Smooth Scroll Support: Auto-detects Lenis to bind
requestAnimationFrameand keep ScrollTrigger synchronized without extra setup. - Native Text Splitting Fixes: Built-in DOM parser preserves formatting tags (
<em>,<span>,<br>), space gaps, and prevents clipping on letter descenders. - Accessibility Focused: Automatically respects
prefers-reduced-motion: reducesettings by bypassing animations for users sensitive to motion. - One-time execution: Uses
once: trueto fire the entrance reveal once as elements enter the viewport. - Safe plugin registration: Guards against missing GSAP/ScrollTrigger dependencies to prevent runtime JavaScript errors.
1<script>
2 document.addEventListener("DOMContentLoaded", function () {
3
4 if (!window.gsap) return;
5
6 gsap.registerPlugin(ScrollTrigger);
7
8 /* ==========================================================
9 LENIS SMOOTH SCROLL
10 (Optional - Only runs if Lenis is loaded)
11 ========================================================== */
12
13 if (window.Lenis) {
14
15 var lenis = new Lenis({
16 lerp: 0.1,
17 smoothWheel: true
18 });
19
20 function raf(time) {
21 lenis.raf(time);
22 requestAnimationFrame(raf);
23 }
24
25 requestAnimationFrame(raf);
26
27 lenis.on("scroll", ScrollTrigger.update);
28
29 }
30
31
32
33
34 /* ==========================================================
35 SPLIT WORD REVEAL
36
37 Usage:
38
39 <h2 data-anim="split-words">
40 Your Heading
41 </h2>
42 ========================================================== */
43
44
45 window.addEventListener("DOMContentLoaded", function () {
46 // 1. Safely register ScrollTrigger plugin (Webflow Template Requirement)
47 if (typeof gsap !== "undefined" && typeof ScrollTrigger !== "undefined") {
48 gsap.registerPlugin(ScrollTrigger);
49 } else {
50 return;
51 }
52
53 // 2. Custom Split-Words Function (Inline Styling & Descender Clipping Fix)
54 function splitWords(el) {
55 if (el.__sp) return null;
56 el.__sp = true;
57
58 if (el.dataset.orig === undefined) {
59 el.dataset.orig = el.innerHTML;
60 }
61
62 var source = document.createElement("div");
63 source.innerHTML = el.dataset.orig;
64
65 var fragment = document.createDocumentFragment();
66 var words = [];
67
68 (function walk(node, italic) {
69 [].forEach.call(node.childNodes, function (child) {
70 if (child.nodeType === 3) {
71 // Text Node
72 child.textContent.split(/\s+/).forEach(function (word) {
73 if (!word) return;
74
75 // Outer wrapper for clipping/masking
76 var wrap = document.createElement("span");
77 wrap.style.display = "inline-block";
78 wrap.style.overflow = "hidden";
79 wrap.style.verticalAlign = "top";
80
81 /* Fixes clipping on descenders (g, y, p, q) & italic slants */
82 wrap.style.paddingBottom = "0.25em";
83 wrap.style.marginBottom = "-0.25em";
84 wrap.style.paddingRight = "0.05em";
85 wrap.style.paddingLeft = "0.05em";
86
87 // Inner wrapper for GSAP transforms
88 var inner = document.createElement("span");
89 inner.style.display = "inline-block";
90 inner.style.willChange = "transform";
91
92 if (italic) {
93 inner.style.fontStyle = "italic";
94 }
95
96 inner.textContent = word;
97 wrap.appendChild(inner);
98
99 fragment.appendChild(wrap);
100 fragment.appendChild(document.createTextNode(" ")); // Preserve spaces
101
102 words.push(inner);
103 });
104 } else if (child.nodeType === 1) {
105 // Element Node
106 if (child.tagName === "BR") {
107 fragment.appendChild(document.createElement("br"));
108 return;
109 }
110
111 walk(
112 child,
113 italic ||
114 child.tagName === "EM" ||
115 child.tagName === "SPAN" ||
116 (child.classList && child.classList.contains("rt-em"))
117 );
118 }
119 });
120 })(source, false);
121
122 el.innerHTML = "";
123 el.appendChild(fragment);
124
125 return words;
126 }
127
128 // Respect Accessibility: Skip animation if user prefers reduced motion
129 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
130 return;
131 }
132
133 // 3. Target and Animate Webflow Elements
134 var targets = gsap.utils.toArray('[data-anim="split-words"]');
135 if (!targets.length) return;
136
137 targets.forEach(function (el) {
138 var words = splitWords(el);
139 if (!words || !words.length) return;
140
141 // Set initial transform state
142 gsap.set(words, { yPercent: 110 });
143
144 ScrollTrigger.create({
145 trigger: el,
146 start: "top 92%",
147 once: true,
148 onEnter: function () {
149 gsap.to(words, {
150 yPercent: 0,
151 duration: 1,
152 stagger: 0.045,
153 ease: "power4.out"
154 });
155 }
156 });
157 });
158 });
159
160 /* ==========================================================
161 REFRESH SCROLLTRIGGER
162 ========================================================== */
163
164 ScrollTrigger.refresh();
165
166
167 // ================================================
168
169 });
170</script>FAQ accordion animation
This interaction creates a smooth accordion animation for FAQ sections, allowing users to expand and collapse answers with a single click.
The animation targets FAQ elements using custom attributes and synchronizes the answer reveal with a rotating icon for a polished user experience.
- The FAQ wrapper uses the attribute
faq="item". - The clickable icon uses the attribute
faq="icon". - The answer container uses the attribute
faq="answer". - Clicking an FAQ item expands its answer while rotating the icon from
0°to90°. - Clicking the item again collapses the answer and returns the icon to its original position.
Customization options
- Use the attribute
faq="item"to define each FAQ accordion item. - Use the attribute
faq="icon"for the rotating indicator.- Default rotation:
0° → 90° - You can change the rotation angle to match your design.
- Default rotation:
- Use the attribute
faq="answer"for the collapsible content.- Default animation:
height: 0 → auto - You can also animate opacity for a softer reveal.
- Default animation:
- Adjust the animation duration.
- Default:
0.5s - Lower value → faster interaction
- Higher value → smoother transition
- Default:
- Modify the easing style.
power2.out→ smooth and responsivepower3.out→ softer finishnone→ linear animation
Features
- Smooth accordion open and close animation
- Animated icon rotation from
0°to90° - Expands answer height from
0toauto - Supports multiple FAQ items using reusable attributes
- Clean and lightweight GSAP implementation
- Easy to customize timing, easing, and rotation angle
- Improves readability and user experience for FAQ sections
Marquee
This animation creates a continuous horizontal scrolling effect by moving elements from their original position to the left across the screen.
The animation targets elements with the attribute visiond= "marquee" and transitions the X position from 0% to -100%, creating a seamless marquee movement commonly used for logos, announcements, or looping text sections.
Customization options
- Use the attribute
data= "marquee"to apply the animation. - Adjust the animation speed by changing the duration.
- Lower duration → faster movement
- Higher duration → slower movement
- Modify the movement direction by changing the X values.
0% → -100%→ left scroll0% → 100%→ right scroll
- Enable infinite looping for continuous movement.
- Add linear easing for a consistent scrolling speed.
- Recommended:
ease: "none"
- Recommended:
- Combine with duplicated content for a seamless infinite marquee effect.
Features
- Smooth infinite horizontal scrolling animation
- Ideal for logo strips, text banners, and announcements
- Fully customizable speed and direction
- Lightweight and performance-friendly
- Creates dynamic movement and visual engagement