Access the AI Animation Generator and premium effects
Upgrade your plan to keep generating animations and unlock premium effects.
Supercharge your Webflow projects with the industry-leading animation library. Master custom code integration, ScrollTrigger, page transitions, and CMS animations.
Webflow is a powerful visual web builder, and its native Interactions are great for basic animations. But GSAP is the professional-grade animation library used by Disney, Apple, Google, and thousands of agencies worldwide. Here's why you should add GSAP to your Webflow toolkit.
Webflow acquired GreenSock in 2024 — all GSAP plugins are now 100% FREE
| Feature | Webflow Interactions | GSAP |
|---|---|---|
| Visual Interface | Yes | Code-based |
| Scroll Animations | Yes | More powerful |
| Timeline Sequencing | Limited | Advanced |
| Text Splitting | No | SplitText plugin |
| SVG Morphing | No | MorphSVG plugin |
| Physics Simulations | No | Draggable, Physics |
| Custom Easing | Limited presets | Full control |
| Performance | Good | Optimized |
| Code Control | None | Full flexibility |
| Community Ecosystem | Webflow-specific | Massive (web-wide) |
GSAP uses requestAnimationFrame and GPU acceleration for silky-smooth 60fps animations even with thousands of animated properties.
Write JavaScript code with complete control over timing, sequencing, easing, and responsive behavior.
ScrollTrigger, SplitText, MorphSVG, Draggable, Physics, and more—all free in Webflow.
Thousands of tutorials, CodePen examples, and a supportive community across the entire web industry.
Adding GSAP to your Webflow project takes less than 5 minutes. You have three options: use Webflow's built-in GSAP toggle, add CDN scripts via Site Settings, or use page-level custom code.
In Webflow (late 2024+), there's a toggle in Site Settings to enable GSAP plugins directly. This is the easiest method.
In your Webflow dashboard, click the Settings icon (gear) → Integrations
Look for "GSAP" toggle and enable ScrollTrigger, SplitText, and any other plugins you need
GSAP is now globally available in your custom code. Use Webflow.push() to run code when the DOM is ready.
If you're on an older Webflow plan or need a specific version, add scripts manually via Site Settings → Custom Code.
Paste this in Site Settings → Custom Code → Head (optional, but good for early loading)
<!-- GSAP Library -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
Go to Site Settings → Custom Code → Before </body> and add:
<!-- ScrollTrigger Plugin -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
<!-- SplitText Plugin (Optional) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/SplitText.min.js"></script>
<!-- MorphSVG Plugin (Optional) -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/MorphSVGPlugin.min.js"></script>
Add this code to register the plugins with GSAP:
gsap.registerPlugin(ScrollTrigger, SplitText, MorphSVGPlugin);
Always load GSAP core before plugins. Place the main gsap.min.js script first, then plugin scripts, then registration code.
This is critical when using GSAP in Webflow. Webflow has a specific initialization pipeline, and you must wait for the DOM to be fully ready before accessing Webflow elements.
Webflow includes a global Webflow object with a push() method. This ensures your code runs after Webflow has initialized its classes, components, and interactions. If you run GSAP code before this happens, selectors might fail to find elements.
Webflow.push(function() {
// Your GSAP code runs here after Webflow is ready
gsap.to('.my-element', { duration: 1, opacity: 1 });
});
You can call Webflow.push() multiple times. Each callback will execute in order when Webflow is ready.
// Initialize animations
Webflow.push(function() {
console.log('Webflow is ready!');
// Hero animation
gsap.from('.wf-hero h1', {
duration: 1,
opacity: 0,
y: 30,
});
});
// Initialize ScrollTrigger
Webflow.push(function() {
// ScrollTrigger code here
gsap.registerPlugin(ScrollTrigger);
});
For larger projects, define functions outside Webflow.push() and call them inside:
// Define animation functions
function initHeroAnimation() {
gsap.from('.wf-hero h1', {
duration: 1.2,
opacity: 0,
y: 50,
ease: 'power3.out'
});
}
function initScrollAnimations() {
gsap.registerPlugin(ScrollTrigger);
gsap.utils.toArray('.wf-fade-in').forEach(element => {
gsap.from(element, {
scrollTrigger: {
trigger: element,
start: 'top 80%',
markers: false
},
opacity: 0,
y: 30,
duration: 0.8
});
});
}
// Run when Webflow is ready
Webflow.push(function() {
initHeroAnimation();
initScrollAnimations();
});
If Webflow.push() isn't available on your plan, use native DOM events:
// Fallback if Webflow.push() not available
if (window.Webflow && typeof Webflow.push === 'function') {
Webflow.push(function() {
// GSAP code here
});
} else {
document.addEventListener('DOMContentLoaded', function() {
// GSAP code here
});
}
Never run GSAP code in a script tag at the bottom of custom code without Webflow.push(). The DOM elements might not exist yet, causing your selectors to fail silently.
Webflow adds special class prefixes and data attributes that make it easy to target elements. Let's learn how to select and animate them with GSAP.
Webflow uses .w- prefixes for built-in classes. You can target these like any CSS selector.
Webflow.push(function() {
// Target by custom class
gsap.to('.my-box', { duration: 1, opacity: 1 });
// Target by Webflow navbar
gsap.to('.w-nav-menu', { duration: 0.5, height: 'auto' });
// Target by element ID
gsap.to('#my-hero', { duration: 1, y: -50 });
// Target multiple elements
gsap.to('.feature-card', {
duration: 0.8,
opacity: 1,
stagger: 0.2 // 0.2s delay between each element
});
});
Add custom data attributes to your Webflow elements to create flexible, reusable animations:
<!-- Add data attributes in Webflow Designer -->
<div class="card" data-animation="fade-up">
<h3>Card Title</h3>
</div>
Webflow.push(function() {
// Target elements by data attribute
const fadeUpElements = document.querySelectorAll('[data-animation="fade-up"]');
fadeUpElements.forEach((element, index) => {
gsap.from(element, {
opacity: 0,
y: 30,
duration: 0.8,
delay: index * 0.1,
ease: 'power2.out'
});
});
});
For cleaner code, use GSAP's built-in gsap.utils.toArray() to select multiple elements:
Webflow.push(function() {
// Animate all feature cards with stagger
gsap.utils.toArray('.feature-card').forEach((card, index) => {
gsap.from(card, {
opacity: 0,
y: 50,
duration: 0.8,
delay: index * 0.15,
ease: 'power3.out'
});
});
// Alternative: Use stagger parameter (simpler)
gsap.from('.feature-card', {
opacity: 0,
y: 50,
duration: 0.8,
stagger: 0.15,
ease: 'power3.out'
});
});
Webflow.push(function() {
const buttons = gsap.utils.toArray('a.button');
buttons.forEach(button => {
// Mouse enter
button.addEventListener('mouseenter', () => {
gsap.to(button, {
scale: 1.05,
duration: 0.3,
overwrite: 'auto'
});
});
// Mouse leave
button.addEventListener('mouseleave', () => {
gsap.to(button, {
scale: 1,
duration: 0.3,
overwrite: 'auto'
});
});
});
});
Webflow.push(function() {
// Animate numbers from 0 to target
gsap.utils.toArray('[data-count]').forEach(element => {
const targetValue = parseInt(element.dataset.count);
gsap.to(element, {
innerHTML: Math.floor(targetValue),
duration: 2,
snap: { innerHTML: 1 },
ease: 'power2.out',
scrollTrigger: {
trigger: element,
start: 'top 80%'
}
});
});
});
Webflow.push(function() {
// Animate list items on scroll
gsap.from('.list-item', {
opacity: 0,
x: -30,
duration: 0.6,
stagger: 0.1,
scrollTrigger: {
trigger: '.list-container',
start: 'top 80%'
}
});
});
Use overwrite: 'auto' when combining hover animations with scroll triggers to prevent conflicts.
ScrollTrigger is the gold standard for scroll-based animations. It's more powerful than Webflow's native scroll interactions and handles complex scenarios beautifully.
First, ensure ScrollTrigger is registered with GSAP:
Webflow.push(function() {
// Register the ScrollTrigger plugin
gsap.registerPlugin(ScrollTrigger);
// Now you can use ScrollTrigger in your animations
});
The most common use case: elements fade in as they scroll into view.
Webflow.push(function() {
gsap.registerPlugin(ScrollTrigger);
// Fade in sections as they scroll into view
gsap.from('.wf-section', {
opacity: 0,
y: 50,
duration: 0.8,
stagger: 0.2,
scrollTrigger: {
trigger: '.wf-section',
start: 'top 80%', // When top of element is 80% down the viewport
end: 'top 50%', // When top of element is 50% down
// markers: true // Uncomment to debug (shows start/end points)
}
});
});
Pin a section to the viewport while animating content inside it:
Webflow.push(function() {
gsap.registerPlugin(ScrollTrigger);
gsap.to('.hero-content', {
opacity: 0.3,
y: -100,
duration: 1,
scrollTrigger: {
trigger: '.hero-section',
start: 'top top',
end: 'bottom top',
scrub: 1, // Smooth scrubbing (1 = 1 second delay)
pin: true, // Pin the section
pinSpacing: true // Account for pinned element height
}
});
});
Scrub animations are tied directly to scroll position—perfect for revealing content:
Webflow.push(function() {
gsap.registerPlugin(ScrollTrigger);
// Create a timeline that plays as you scroll
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.timeline-section',
start: 'top center',
end: 'bottom center',
scrub: true, // true = instant, 1-3 = smooth
markers: false
}
});
tl.from('.step-1', { opacity: 0, y: 50, duration: 1 })
.from('.step-2', { opacity: 0, y: 50, duration: 1 }, '-=0.5')
.from('.step-3', { opacity: 0, y: 50, duration: 1 }, '-=0.5');
});
Create depth with parallax scrolling:
Webflow.push(function() {
gsap.registerPlugin(ScrollTrigger);
// Background image moves slower than foreground
gsap.to('.parallax-bg', {
y: -200,
ease: 'none',
scrollTrigger: {
trigger: '.parallax-section',
start: 'top top',
end: 'bottom top',
scrub: 1
}
});
});
Learn more: See our dedicated ScrollTrigger Guide
The main challenge with CMS items is that they're rendered dynamically. You need to target them correctly and handle cases where items load after the initial page.
Webflow automatically adds these classes to CMS elements:
<!-- Webflow auto-generates these classes -->
<div class="w-dyn-items"> <!-- Container for all items -->
<div class="w-dyn-item"> <!-- Individual CMS item -->
<div class="card">
<h3 class="card-title">Item Title</h3>
<p class="card-text">Description</p>
</div>
</div>
<div class="w-dyn-item">
<!-- More items... -->
</div>
</div>
Webflow.push(function() {
gsap.registerPlugin(ScrollTrigger);
// Select all CMS items and animate them
gsap.from('.w-dyn-item', {
opacity: 0,
y: 30,
duration: 0.6,
stagger: 0.15,
scrollTrigger: {
trigger: '.w-dyn-items',
start: 'top 80%'
}
});
});
If your CMS items load dynamically (e.g., infinite scroll, pagination), use MutationObserver to detect and animate new items:
Webflow.push(function() {
gsap.registerPlugin(ScrollTrigger);
function animateCMSItems() {
gsap.from('.w-dyn-item', {
opacity: 0,
y: 30,
duration: 0.6,
stagger: 0.1,
overwrite: 'auto'
});
}
// Animate initial items
animateCMSItems();
// Watch for new items
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.addedNodes.length) {
animateCMSItems();
ScrollTrigger.refresh(); // Update scroll triggers
}
});
});
observer.observe(document.querySelector('.w-dyn-items'), {
childList: true
});
});
Create a timeline effect where items appear in sequence as the user scrolls:
Webflow.push(function() {
gsap.registerPlugin(ScrollTrigger);
const items = gsap.utils.toArray('.w-dyn-item');
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.w-dyn-items',
start: 'top 70%',
end: 'center center'
}
});
items.forEach((item, index) => {
tl.from(item, {
opacity: 0,
x: -50,
duration: 0.5
}, index * 0.1);
});
});
Target specific fields within CMS items:
Webflow.push(function() {
gsap.registerPlugin(ScrollTrigger);
// Animate only the title within each CMS item
gsap.from('.w-dyn-item .card-title', {
opacity: 0,
y: 20,
duration: 0.5,
stagger: 0.1,
scrollTrigger: {
trigger: '.w-dyn-items',
start: 'top 80%'
}
});
// Then animate the description slightly after
gsap.from('.w-dyn-item .card-text', {
opacity: 0,
y: 20,
duration: 0.5,
stagger: 0.1,
delay: 0.2,
scrollTrigger: {
trigger: '.w-dyn-items',
start: 'top 80%'
}
});
});
Always use .w-dyn-item and .w-dyn-items selectors for CMS items, not custom classes. This ensures compatibility across all CMS configurations.
Create smooth transitions between Webflow pages with exit and enter animations. This requires a bit more setup but feels incredibly polished.
When a user clicks a link:
Use preventDefault() and AJAX-like page loads:
Webflow.push(function() {
// Fade out and navigate
document.querySelectorAll('a').forEach(link => {
link.addEventListener('click', function(e) {
// Don't intercept external links or anchors
if (this.hostname === location.hostname &&
!this.href.includes('#') &&
!this.classList.contains('no-transition')) {
e.preventDefault();
const href = this.href;
// Fade out animation
gsap.to('body', {
opacity: 0,
duration: 0.5,
onComplete: () => {
window.location.href = href;
}
});
}
});
});
// Fade in on page load
gsap.from('body', {
opacity: 0,
duration: 0.5
});
});
Separate animations for leaving and entering pages:
Webflow.push(function() {
// Exit animation when leaving page
function exitAnimation() {
return gsap.to('main', {
opacity: 0,
y: 50,
duration: 0.5,
ease: 'power2.in'
});
}
// Enter animation when arriving on page
function enterAnimation() {
return gsap.from('main', {
opacity: 0,
y: 50,
duration: 0.5,
ease: 'power2.out'
});
}
// Hook up exit animation to links
document.querySelectorAll('a:not(.no-transition)').forEach(link => {
link.addEventListener('click', function(e) {
if (this.hostname === location.hostname &&
!this.href.includes('#')) {
e.preventDefault();
const href = this.href;
exitAnimation().then(() => {
window.location.href = href;
});
}
});
});
// Play enter animation on page load
enterAnimation();
});
Show a preloader while the page loads:
Webflow.push(function() {
const preloader = document.querySelector('.page-preloader');
// Animate preloader in on transition
function showPreloader() {
return gsap.to(preloader, {
opacity: 1,
pointerEvents: 'auto',
duration: 0.3
});
}
// Hide preloader when page loads
function hidePreloader() {
return gsap.to(preloader, {
opacity: 0,
pointerEvents: 'none',
duration: 0.5,
delay: 0.5
});
}
// On page load, hide preloader
window.addEventListener('load', () => {
hidePreloader();
});
// On link click, show preloader and transition
document.querySelectorAll('a:not(.no-transition)').forEach(link => {
link.addEventListener('click', function(e) {
if (this.hostname === location.hostname &&
!this.href.includes('#')) {
e.preventDefault();
showPreloader().then(() => {
setTimeout(() => {
window.location.href = this.href;
}, 300);
});
}
});
});
});
For production page transitions, consider using barba.js, a lightweight AJAX library designed for smooth page transitions. It pairs beautifully with GSAP and handles many edge cases automatically.
Add class no-transition to external links, download buttons, and form submits to skip the transition.
Now that you've mastered the basics, let's explore advanced techniques used by top agencies.
Webflow.push(function() {
gsap.registerPlugin(ScrollTrigger);
const container = document.querySelector('.h-scroll-container');
const cards = container.querySelectorAll('.card');
const totalScroll = cards.length * 300; // Each card is ~300px
gsap.to(container, {
x: -totalScroll,
ease: 'none',
scrollTrigger: {
trigger: '.h-scroll-section',
start: 'top top',
end: 'bottom top',
scrub: 1,
pin: true,
markers: false
}
});
});
Webflow.push(function() {
gsap.registerPlugin(SplitText);
const headings = gsap.utils.toArray('h2');
headings.forEach(heading => {
const splitText = new SplitText(heading, {
type: 'chars'
});
gsap.from(splitText.chars, {
opacity: 0,
y: 20,
duration: 0.8,
stagger: 0.05,
scrollTrigger: {
trigger: heading,
start: 'top 80%'
}
});
});
});
Webflow.push(function() {
gsap.registerPlugin(MorphSVGPlugin);
const shape = document.querySelector('.morph-shape');
gsap.to(shape, {
attr: {
d: document.querySelector('.morph-target').getAttribute('d')
},
duration: 2,
ease: 'power2.inOut',
scrollTrigger: {
trigger: '.morph-section',
start: 'top center',
end: 'bottom center',
scrub: 1
}
});
});
Webflow.push(function() {
gsap.registerPlugin(ScrollTrigger);
// Control Lottie playback with scroll
const lottieContainer = document.querySelector('#lottie-animation');
// Make sure Lottie is loaded first
if (window.bodymovin && lottieContainer.innerHTML) {
gsap.to(window, {
scrollTrigger: {
trigger: '.lottie-section',
start: 'top center',
end: 'bottom center',
scrub: 1,
onUpdate: (self) => {
// Control frame based on progress (0-1)
const frame = Math.round(self.progress * 100);
// Call Lottie playback method if available
}
}
});
}
});
Webflow.push(function() {
const cursor = document.querySelector('.custom-cursor');
document.addEventListener('mousemove', (e) => {
gsap.to(cursor, {
x: e.clientX - 10, // Center the cursor
y: e.clientY - 10,
duration: 0.1,
overwrite: 'auto'
});
});
// Make cursor larger on hover
document.querySelectorAll('a, button').forEach(el => {
el.addEventListener('mouseenter', () => {
gsap.to(cursor, { scale: 1.5, duration: 0.2 });
});
el.addEventListener('mouseleave', () => {
gsap.to(cursor, { scale: 1, duration: 0.2 });
});
});
});
Always test advanced animations on both desktop and mobile. Some effects (like custom cursors or horizontal scrolling) need mobile alternatives.
GSAP is incredibly performant, but poor implementation can slow down your site. Here's how to optimize and troubleshoot.
/* In your CSS file */
.animate-element {
will-change: opacity, transform;
}
/* Or add it dynamically with GSAP */
Webflow.push(function() {
gsap.set('.animate-element', {
willChange: 'opacity, transform'
});
// Then animate
gsap.to('.animate-element', {
opacity: 0.5,
duration: 1
});
});
Webflow.push(function() {
// Check if elements exist
const elements = document.querySelectorAll('.my-element');
console.log('Found elements:', elements.length);
// Log GSAP version
console.log('GSAP Version:', gsap.version);
// Check if plugins are registered
console.log('Plugins:', gsap.plugins());
// Enable ScrollTrigger debugging
gsap.registerPlugin(ScrollTrigger);
gsap.to('.my-element', {
y: 50,
scrollTrigger: {
trigger: '.my-element',
markers: true // Show start/end markers
}
});
// Log animation timeline
const tl = gsap.timeline();
tl.to('.element-1', { opacity: 1 });
console.log('Timeline:', tl);
});
Make sure you're using the correct class names and that Webflow.push() is running after the DOM is ready. Check browser console for errors.
Refresh ScrollTrigger after content loads: ScrollTrigger.refresh()
Don't animate the same element with both GSAP and Webflow Interactions. Use GSAP for all custom animations.
Reduce stagger values, simplify animations, and test on actual devices. Desktop is not mobile.
Webflow.push(function() {
// Monitor FPS during animations
let lastTime = performance.now();
let fps = 0;
const checkFPS = () => {
const now = performance.now();
const delta = now - lastTime;
fps = Math.round(1000 / delta);
console.log('FPS:', fps);
lastTime = now;
};
// Check FPS during scroll
window.addEventListener('scroll', checkFPS, { passive: true });
// Clean up after animations complete
gsap.to('.element', {
opacity: 1,
duration: 2,
onComplete: () => {
window.removeEventListener('scroll', checkFPS);
console.log('Animation complete');
}
});
});
GSAP works across all modern browsers, but always test in Chrome, Firefox, Safari, and Edge to catch edge cases.
overwrite: 'auto' in GSAP to prevent overlap.
markers: true to see trigger points. Always test on the published site, not the Designer, as Webflow Designer mode behaves differently.
.w-dyn-items and .w-dyn-item classes. For dynamic loading, use MutationObserver to detect new items and animate them.
gsap.to() animates FROM current state TO the specified state. gsap.from() sets starting values and animates TO the current state. gsap.fromTo() lets you define both the start and end states explicitly.
You now have everything you need to create stunning GSAP animations in Webflow. Start with ScrollTrigger basics and work your way up to advanced patterns.