
Gsap Scrolltrigger
- 1.6k installs
- 629 repo stars
- Updated November 20, 2025
- freshtechbro/claudedesignskills
gsap-scrolltrigger is an agent skill for create gsap and scrolltrigger scroll-driven animations, timelines, and tweens.
About
The gsap-scrolltrigger skill is designed for create GSAP and ScrollTrigger scroll-driven animations, timelines, and tweens. GSAP & ScrollTrigger Development Overview GSAP (GreenSock Animation Platform) is the industry-leading JavaScript animation library for creating high-performance, production-quality animations. ScrollTrigger is GSAP's powerful plugin for scroll-driven animations. Invoke when the user builds web animations, scroll-triggered effects, or GSAP timelines.
- api_reference.md: Quick API reference (tween methods, timeline methods, ScrollTrigger properties).
- easing_guide.md: Visual easing reference with use cases.
- common_patterns.md: Copy-paste patterns for common scenarios.
- generate_animation.py: Generate boilerplate GSAP code.
- timeline_builder.py: Interactive timeline sequence builder.
Gsap Scrolltrigger by the numbers
- 1,612 all-time installs (skills.sh)
- +111 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #284 of 2,244 Frontend Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
gsap-scrolltrigger capabilities & compatibility
- Capabilities
- api_reference.md: quick api reference (tween met · easing_guide.md: visual easing reference with us · common_patterns.md: copy paste patterns for comm · generate_animation.py: generate boilerplate gsap
- Use cases
- frontend
What gsap-scrolltrigger says it does
Comprehensive skill for GSAP (GreenSock Animation Platform) and ScrollTrigger plugin. Use this skill when creating web animations, scroll-driven experiences, timelines, tweens, scr
Comprehensive skill for GSAP (GreenSock Animation Platform) and ScrollTrigger plugin. Use this skill when creating web animations, scroll-driven experiences, ti
npx skills add https://github.com/freshtechbro/claudedesignskills --skill gsap-scrolltriggerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 629 |
| Security audit | 3 / 3 scanners passed |
| Last updated | November 20, 2025 |
| Repository | freshtechbro/claudedesignskills ↗ |
How do I create gsap and scrolltrigger scroll-driven animations, timelines, and tweens?
Create GSAP and ScrollTrigger scroll-driven animations, timelines, and tweens.
Who is it for?
Frontend developers building scroll animations and GreenSock timeline experiences.
Skip if: Skip for CSS-only transitions without GSAP or ScrollTrigger.
When should I use this skill?
User builds web animations, scroll-triggered effects, or GSAP timelines.
What you get
Completed gsap-scrolltrigger workflow with documented commands, files, and expected deliverables.
- ScrollTrigger configuration code
- Timeline animation snippets
By the numbers
- 801 installs on skills.sh
Files
GSAP & ScrollTrigger Development
Overview
GSAP (GreenSock Animation Platform) is the industry-leading JavaScript animation library for creating high-performance, production-quality animations. ScrollTrigger is GSAP's powerful plugin for scroll-driven animations. Together, they enable everything from simple UI transitions to complex scroll-based storytelling experiences.
Core Concepts
The Basics: Tweens
A tween is a single animation from point A to point B.
// Animate TO a state (from current)
gsap.to(".box", {
x: 200,
rotation: 360,
duration: 1,
ease: "power2.inOut"
});
// Animate FROM a state (to current)
gsap.from(".box", {
opacity: 0,
y: -50,
duration: 0.8
});
// Animate FROM-TO (define both start and end)
gsap.fromTo(".box",
{ opacity: 0, scale: 0.5 }, // FROM
{ opacity: 1, scale: 1, duration: 1 } // TO
);Timelines: Sequencing Animations
Timelines orchestrate multiple tweens in sequence or overlap.
const tl = gsap.timeline();
// Sequential by default
tl.to(".box1", { x: 100, duration: 1 })
.to(".box2", { y: 100, duration: 1 })
.to(".box3", { rotation: 360, duration: 1 });
// With labels for organization
tl.addLabel("start")
.to(".hero", { opacity: 1, duration: 1 })
.addLabel("reveal")
.to(".content", { y: 0, duration: 0.8 }, "reveal") // Start at "reveal" label
.to(".cta", { scale: 1, duration: 0.5 }, "reveal+=0.5"); // 0.5s after "reveal"Position Parameter (Timeline Timing)
Control when animations start within a timeline:
const tl = gsap.timeline();
// Default: One after another
tl.to(".box1", { x: 100 })
.to(".box2", { x: 100 }); // Starts after box1 finishes
// Start at the same time
tl.to(".box1", { x: 100 })
.to(".box2", { y: 100 }, 0); // Starts at 0 seconds
// Relative positioning
tl.to(".box1", { x: 100, duration: 2 })
.to(".box2", { y: 100 }, "-=1"); // Starts 1 second before box1 ends
.to(".box3", { rotation: 360 }, "+=0.5"); // Starts 0.5s after box2 finishes
// At a specific time
tl.to(".box1", { x: 100 }, 2.5); // Starts at 2.5 secondsScrollTrigger Fundamentals
Basic Scroll Animation
gsap.registerPlugin(ScrollTrigger);
gsap.to(".box", {
x: 500,
scrollTrigger: {
trigger: ".box",
start: "top center", // When top of trigger hits center of viewport
end: "bottom center",
markers: true, // Development only - shows start/end positions
scrub: true, // Links animation to scrollbar
toggleActions: "play none none reverse" // onEnter onLeave onEnterBack onLeaveBack
}
});Start & End Positions
Format: "[trigger position] [viewport position]"
// Common patterns
start: "top top" // Trigger top hits viewport top
start: "top center" // Trigger top hits viewport center (default)
start: "top bottom" // Trigger top hits viewport bottom
start: "center center" // Trigger center hits viewport center
// With offsets
start: "top top+=100" // 100px below viewport top
start: "top 80%" // 80% down the viewport
end: "+=500" // 500px after start position
end: "bottom top" // Trigger bottom hits viewport topScrubbing (Scroll-Synced Animation)
// Boolean: Direct link to scrollbar (immediate)
scrub: true
// Number: Smoothing delay in seconds
scrub: 1 // Takes 1 second to "catch up" to scrollbar
scrub: 0.5 // Faster, tighter feelToggle Actions
Control animation at four scroll points:
toggleActions: "play pause resume reset"
// onEnter | onLeave | onEnterBack | onLeaveBack
// Actions: play, pause, resume, restart, reset, complete, reverse, noneCommon patterns:
toggleActions: "play none none none" // Play once on enter
toggleActions: "play none none reverse" // Play forward, reverse back
toggleActions: "play complete reverse reset" // Full control
toggleActions: "restart pause resume pause" // Restart on each enterCommon Patterns
1. Fade In On Scroll
gsap.from(".fade-in", {
opacity: 0,
y: 50,
duration: 1,
scrollTrigger: {
trigger: ".fade-in",
start: "top 80%",
end: "top 50%",
scrub: 1,
once: true // Only animate once
}
});2. Pin Element While Scrolling
ScrollTrigger.create({
trigger: ".panel",
start: "top top",
end: "+=500", // Pin for 500px of scrolling
pin: true,
pinSpacing: true // Add spacing (default true)
});3. Horizontal Scroll Section
const sections = gsap.utils.toArray(".panel");
gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: "none",
scrollTrigger: {
trigger: ".container",
pin: true,
scrub: 1,
end: () => "+=" + document.querySelector(".container").offsetWidth
}
});4. Parallax Effect
// Slower movement (background layer)
gsap.to(".bg", {
y: 200,
ease: "none",
scrollTrigger: {
trigger: ".section",
start: "top bottom",
end: "bottom top",
scrub: true
}
});
// Faster movement (foreground layer)
gsap.to(".fg", {
y: -100,
ease: "none",
scrollTrigger: {
trigger: ".section",
start: "top bottom",
end: "bottom top",
scrub: true
}
});5. Scroll-Triggered Timeline
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".container",
start: "top top",
end: "+=500",
scrub: 1,
pin: true,
snap: {
snapTo: "labels", // Snap to timeline labels
duration: { min: 0.2, max: 3 },
delay: 0.2,
ease: "power1.inOut"
}
}
});
tl.addLabel("start")
.from(".title", { scale: 0.3, rotation: 45, autoAlpha: 0 })
.addLabel("color")
.from(".box", { backgroundColor: "#28a92b" })
.addLabel("spin")
.to(".box", { rotation: 360 })
.addLabel("end");6. Batch Animations (Multiple Elements)
// Loop through multiple elements
gsap.utils.toArray(".box").forEach((box, i) => {
gsap.from(box, {
y: 100,
opacity: 0,
scrollTrigger: {
trigger: box,
start: "top 80%",
end: "top 50%",
scrub: 1
}
});
});
// Or use ScrollTrigger.batch
ScrollTrigger.batch(".box", {
onEnter: batch => gsap.to(batch, { opacity: 1, y: 0, stagger: 0.15 }),
onLeave: batch => gsap.set(batch, { opacity: 0 }),
start: "top 80%",
once: true
});7. Staggered Animations
gsap.from(".item", {
y: 50,
opacity: 0,
duration: 0.8,
stagger: 0.1, // 0.1s between each item
scrollTrigger: {
trigger: ".grid",
start: "top 80%"
}
});
// Advanced stagger
gsap.from(".item", {
scale: 0,
duration: 1,
stagger: {
each: 0.1,
from: "center", // "start", "center", "end", "edges", or index number
grid: "auto", // For grid layouts
ease: "power2.inOut"
}
});Integration Patterns
With Three.js / WebGL
import * as THREE from 'three';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
// Animate camera
gsap.to(camera.position, {
x: 5,
y: 3,
z: 10,
scrollTrigger: {
trigger: "#section2",
start: "top top",
end: "bottom top",
scrub: 1,
onUpdate: () => camera.lookAt(scene.position)
}
});
// Animate mesh rotation
gsap.to(mesh.rotation, {
y: Math.PI * 2,
scrollTrigger: {
trigger: "#section3",
start: "top bottom",
end: "bottom top",
scrub: true
}
});
// Animate material properties
gsap.to(material, {
opacity: 0,
scrollTrigger: {
trigger: "#section4",
start: "top center",
end: "center center",
scrub: 1
}
});With React (useGSAP Hook)
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
function Component() {
const container = useRef();
const box = useRef();
useGSAP(() => {
gsap.to(box.current, {
x: 200,
scrollTrigger: {
trigger: box.current,
start: "top center",
end: "bottom center",
scrub: true,
markers: true
}
});
}, { scope: container }); // Scoping for cleanup
return (
<div ref={container}>
<div ref={box} className="box">Animated Box</div>
</div>
);
}Sharing Timeline in React
function App() {
const [tl, setTl] = useState();
useGSAP(() => {
const timeline = gsap.timeline();
setTl(timeline);
}, []);
return (
<div>
<Box timeline={tl} index={0} />
<Circle timeline={tl} index={1} />
</div>
);
}
function Box({ timeline, index }) {
const ref = useRef();
useGSAP(() => {
timeline && timeline.to(ref.current, { x: 100 }, index * 0.1);
}, [timeline, index]);
return <div ref={ref} className="box" />;
}Locomotive Scroll Integration
import LocomotiveScroll from 'locomotive-scroll';
const scroller = new LocomotiveScroll({
el: document.querySelector('[data-scroll-container]'),
smooth: true
});
ScrollTrigger.scrollerProxy("[data-scroll-container]", {
scrollTop(value) {
return arguments.length ? scroller.scrollTo(value, 0, 0) : scroller.scroll.instance.scroll.y;
},
getBoundingClientRect() {
return {top: 0, left: 0, width: window.innerWidth, height: window.innerHeight};
},
pinType: document.querySelector("[data-scroll-container]").style.transform ? "transform" : "fixed"
});
ScrollTrigger.addEventListener("refresh", () => scroller.update());
ScrollTrigger.refresh();Advanced Techniques
Image Sequence Scrubbing
const canvas = document.querySelector("canvas");
const context = canvas.getContext("2d");
const images = [];
const imageCount = 147;
const currentFrame = { value: 0 };
for (let i = 0; i < imageCount; i++) {
const img = new Image();
img.src = `./frames/frame_${i.toString().padStart(4, '0')}.jpg`;
images.push(img);
}
images[0].onload = () => {
canvas.width = images[0].width;
canvas.height = images[0].height;
render();
};
function render() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(images[Math.floor(currentFrame.value)], 0, 0);
}
gsap.to(currentFrame, {
value: imageCount - 1,
snap: "value",
ease: "none",
scrollTrigger: {
trigger: canvas,
start: "top top",
end: "+=500%",
scrub: true,
pin: true
},
onUpdate: render
});Smooth Scroll to Element
gsap.registerPlugin(ScrollToPlugin);
// Scroll to element
gsap.to(window, {
duration: 1,
scrollTo: "#section2",
ease: "power2.inOut"
});
// With offset
gsap.to(window, {
duration: 1.5,
scrollTo: { y: "#section2", offsetY: 50 },
ease: "expo.inOut"
});
// Horizontal scroll
gsap.to(".container", {
duration: 2,
scrollTo: { x: 1000, autoKill: true }
});Conditional Animations (Media Queries)
ScrollTrigger.matchMedia({
// Desktop
"(min-width: 800px)": function() {
gsap.to(".box", {
x: 500,
scrollTrigger: {
trigger: ".box",
start: "top center",
end: "bottom top",
scrub: true
}
});
},
// Mobile
"(max-width: 799px)": function() {
gsap.to(".box", {
y: 200,
scrollTrigger: {
trigger: ".box",
start: "top 80%",
scrub: 1
}
});
}
});Performance Best Practices
1. Use will-change CSS
.animated-element {
will-change: transform, opacity;
}2. Limit Repaints
// Good: Animate transform/opacity (GPU accelerated)
gsap.to(".box", { x: 100, opacity: 0.5 });
// Avoid: Animating layout properties
// gsap.to(".box", { width: 500, height: 300 }); // Causes reflow3. Dispose of ScrollTriggers
// Kill individual trigger
const trigger = ScrollTrigger.create({ /* ... */ });
trigger.kill();
// Kill all triggers
ScrollTrigger.getAll().forEach(t => t.kill());
// In React with cleanup
useGSAP(() => {
const tween = gsap.to(".box", { /* ... */ });
return () => {
tween.kill();
};
}, []);4. Debounce Resize
ScrollTrigger handles this automatically, but for custom resize logic:
let resizeTimer;
window.addEventListener("resize", () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
ScrollTrigger.refresh();
}, 250);
});5. Use invalidateOnRefresh
For dynamic values that change on resize:
gsap.to(".box", {
x: () => window.innerWidth / 2, // Dynamic value
scrollTrigger: {
trigger: ".box",
start: "top center",
invalidateOnRefresh: true // Recalculate x on resize
}
});Common Pitfalls
1. Multiple Tweens on Same Element
// Problem: Second tween conflicts with first
gsap.to('h1', { x: 100, scrollTrigger: { /* ... */ } });
gsap.to('h1', { x: 200, scrollTrigger: { /* ... */ } }); // Jumps!
// Solution 1: Use fromTo
gsap.fromTo('h1', { x: 100 }, { x: 200, scrollTrigger: { /* ... */ } });
// Solution 2: Use immediateRender: false
gsap.to('h1', { x: 200, immediateRender: false, scrollTrigger: { /* ... */ } });
// Solution 3: Apply ScrollTrigger to timeline
const tl = gsap.timeline({ scrollTrigger: { /* ... */ } });
tl.to('h1', { x: 100 })
.to('h1', { x: 200 });2. Not Using Loops for Multiple Elements
// Wrong: Animates all at once
gsap.to('.section', {
y: -100,
scrollTrigger: { trigger: '.section', scrub: true }
});
// Right: Loop for individual triggers
gsap.utils.toArray('.section').forEach(section => {
gsap.to(section, {
y: -100,
scrollTrigger: { trigger: section, scrub: true }
});
});3. Forgetting to Register Plugins
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { ScrollToPlugin } from 'gsap/ScrollToPlugin';
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin); // Must register!4. Nested ScrollTriggers in Timelines
// Wrong: ScrollTriggers on individual tweens in timeline
const tl = gsap.timeline();
tl.to('.box1', { x: 100, scrollTrigger: { /* ... */ } }) // Don't do this!
.to('.box2', { y: 100, scrollTrigger: { /* ... */ } });
// Right: ScrollTrigger on parent timeline
const tl = gsap.timeline({
scrollTrigger: { /* ... */ }
});
tl.to('.box1', { x: 100 })
.to('.box2', { y: 100 });Easing Reference
// Power easings (most common)
ease: "power1.out" // Subtle deceleration
ease: "power2.inOut" // Smooth acceleration/deceleration
ease: "power3.in" // Strong acceleration
ease: "power4.out" // Very strong deceleration
// Special easings
ease: "elastic.out" // Bouncy overshoot
ease: "back.out" // Slight overshoot
ease: "bounce.out" // Bouncing effect
ease: "circ.inOut" // Circular motion feel
ease: "expo.inOut" // Exponential (dramatic)
// Linear (for scrubbed scroll animations)
ease: "none"ScrollTrigger Methods
// Refresh all ScrollTriggers (after DOM changes)
ScrollTrigger.refresh();
// Get all ScrollTriggers
const triggers = ScrollTrigger.getAll();
// Get specific trigger by ID
const st = ScrollTrigger.getById("myTrigger");
// Kill trigger
st.kill();
// Update trigger
st.scroll(500); // Programmatically set scroll position
st.enable();
st.disable();
// Global ScrollTrigger config
ScrollTrigger.config({
limitCallbacks: true, // Improve performance
syncInterval: 15 // Throttle scroll checks (ms)
});
// Debug mode
ScrollTrigger.defaults({
markers: true // Show markers on all triggers
});Resources
This skill includes bundled resources:
references/
api_reference.md: Quick API reference (tween methods, timeline methods, ScrollTrigger properties)easing_guide.md: Visual easing reference with use casescommon_patterns.md: Copy-paste patterns for common scenarios
scripts/
generate_animation.py: Generate boilerplate GSAP codetimeline_builder.py: Interactive timeline sequence builder
assets/
starter_scroll/: Complete scroll-driven site templateeasings/: Easing visualization HTML toolexamples/: Real-world ScrollTrigger examples
When to Use This Skill
Use this skill when:
- Creating smooth web animations
- Building scroll-driven experiences
- Implementing parallax effects
- Sequencing complex animations
- Animating DOM, SVG, Canvas, or WebGL
- Integrating animations with Three.js or React
- Building scrollytelling websites
- Creating interactive UI transitions
For Three.js-specific animations, also reference the threejs-webgl skill. For React components with built-in animations, reference the motion-framer skill.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GSAP Easing Visualizer</title>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
color: #333;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 40px;
color: white;
}
h1 {
font-size: 3rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
}
.controls {
background: white;
padding: 30px;
border-radius: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
margin-bottom: 40px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.control-group {
display: flex;
flex-direction: column;
}
label {
font-weight: 600;
margin-bottom: 8px;
color: #555;
}
input, select, button {
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
transition: border-color 0.3s;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
}
button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
cursor: pointer;
font-weight: 600;
transition: transform 0.2s, box-shadow 0.2s;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
button:active {
transform: translateY(0);
}
.easing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 30px;
margin-bottom: 40px;
}
.easing-card {
background: white;
padding: 25px;
border-radius: 15px;
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
transition: transform 0.3s, box-shadow 0.3s;
}
.easing-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 30px rgba(0,0,0,0.25);
}
.easing-name {
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 15px;
color: #764ba2;
display: flex;
justify-content: space-between;
align-items: center;
}
.copy-btn {
padding: 5px 10px;
font-size: 0.8rem;
background: #f0f0f0;
color: #333;
border: 1px solid #ddd;
cursor: pointer;
transition: background 0.2s;
}
.copy-btn:hover {
background: #e0e0e0;
}
.track {
width: 100%;
height: 60px;
background: #f5f5f5;
border-radius: 10px;
margin-bottom: 15px;
position: relative;
overflow: hidden;
box-shadow: inset 0 2px 5px rgba(0,0,0,0.1);
}
.box {
width: 50px;
height: 50px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 8px;
position: absolute;
left: 5px;
top: 5px;
box-shadow: 0 3px 10px rgba(0,0,0,0.2);
}
.curve-preview {
width: 100%;
height: 80px;
background: #fafafa;
border-radius: 8px;
position: relative;
border: 2px solid #e0e0e0;
}
.curve-line {
stroke: #764ba2;
stroke-width: 3;
fill: none;
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
}
.use-case {
margin-top: 15px;
padding: 12px;
background: #f9f9f9;
border-radius: 8px;
font-size: 0.9rem;
color: #666;
border-left: 4px solid #667eea;
}
.action-bar {
text-align: center;
margin-bottom: 30px;
}
.action-bar button {
margin: 0 10px;
padding: 15px 30px;
font-size: 1.1rem;
}
.comparison-mode {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
margin-top: 30px;
}
.comparison-card {
background: white;
padding: 20px;
border-radius: 12px;
box-shadow: 0 5px 15px rgba(0,0,0,0.15);
}
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.controls {
grid-template-columns: 1fr;
}
.easing-grid {
grid-template-columns: 1fr;
}
}
.copied-toast {
position: fixed;
bottom: 30px;
right: 30px;
background: #4caf50;
color: white;
padding: 15px 25px;
border-radius: 8px;
box-shadow: 0 5px 20px rgba(0,0,0,0.3);
opacity: 0;
transform: translateY(20px);
transition: opacity 0.3s, transform 0.3s;
pointer-events: none;
z-index: 1000;
}
.copied-toast.show {
opacity: 1;
transform: translateY(0);
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>⚡ GSAP Easing Visualizer</h1>
<p class="subtitle">Explore and compare GSAP easing functions</p>
</header>
<div class="controls">
<div class="control-group">
<label for="duration">Duration (seconds)</label>
<input type="number" id="duration" value="1.5" step="0.1" min="0.1" max="5">
</div>
<div class="control-group">
<label for="distance">Distance (pixels)</label>
<input type="number" id="distance" value="280" step="10" min="50" max="500">
</div>
<div class="control-group">
<label for="category">Category Filter</label>
<select id="category">
<option value="all">All Easings</option>
<option value="power">Power (Most Common)</option>
<option value="special">Special (Back, Elastic, Bounce)</option>
<option value="other">Other (Circ, Expo, Sine)</option>
</select>
</div>
<div class="control-group">
<button id="play-all">▶ Play All</button>
</div>
</div>
<div class="action-bar">
<button id="reset-all">↻ Reset All</button>
<button id="toggle-curves">📊 Toggle Curves</button>
</div>
<div id="easing-container" class="easing-grid"></div>
<div class="copied-toast" id="toast">Copied to clipboard!</div>
</div>
<script>
// Easing functions with metadata
const easings = [
// Power easings
{ name: "none", category: "power", useCase: "Constant speed, scroll-driven animations" },
{ name: "power1.out", category: "power", useCase: "Subtle UI transitions, tooltips" },
{ name: "power2.out", category: "power", useCase: "⭐ Default choice for most animations" },
{ name: "power3.out", category: "power", useCase: "Hero sections, dramatic reveals" },
{ name: "power4.out", category: "power", useCase: "Full-screen transitions, maximum impact" },
{ name: "power1.in", category: "power", useCase: "Rare: elements accelerating off-screen" },
{ name: "power2.in", category: "power", useCase: "Rare: strong acceleration" },
{ name: "power1.inOut", category: "power", useCase: "Smooth start and end" },
{ name: "power2.inOut", category: "power", useCase: "Balanced animation both ends" },
{ name: "power3.inOut", category: "power", useCase: "Strong smooth both ends" },
// Special easings
{ name: "back.out(1.7)", category: "special", useCase: "Playful buttons, fun micro-interactions" },
{ name: "back.out(2.5)", category: "special", useCase: "Exaggerated overshoot" },
{ name: "back.in(1.7)", category: "special", useCase: "Element backs up before leaving" },
{ name: "back.inOut(1.7)", category: "special", useCase: "Overshoot both ends" },
{ name: "elastic.out(1, 0.3)", category: "special", useCase: "Spring effect, notification badges" },
{ name: "elastic.in(1, 0.3)", category: "special", useCase: "Rare: elastic entrance" },
{ name: "bounce.out", category: "special", useCase: "Elements dropping into place" },
{ name: "bounce.in", category: "special", useCase: "Bouncing ball entering" },
// Other easings
{ name: "circ.out", category: "other", useCase: "Fast snappy movements, Material Design" },
{ name: "circ.in", category: "other", useCase: "Sharp acceleration" },
{ name: "circ.inOut", category: "other", useCase: "Sharp both ends" },
{ name: "expo.out", category: "other", useCase: "Dramatic zoom effects, cinematic" },
{ name: "expo.in", category: "other", useCase: "Extremely slow start" },
{ name: "expo.inOut", category: "other", useCase: "Intense page transitions" },
{ name: "sine.out", category: "other", useCase: "Subtle parallax, ambient effects" },
{ name: "sine.in", category: "other", useCase: "Very gentle acceleration" },
{ name: "sine.inOut", category: "other", useCase: "Barely noticeable animations" },
];
let duration = 1.5;
let distance = 280;
let showCurves = true;
const container = document.getElementById('easing-container');
const durationInput = document.getElementById('duration');
const distanceInput = document.getElementById('distance');
const categorySelect = document.getElementById('category');
const playAllBtn = document.getElementById('play-all');
const resetAllBtn = document.getElementById('reset-all');
const toggleCurvesBtn = document.getElementById('toggle-curves');
const toast = document.getElementById('toast');
// Generate easing cards
function renderEasings() {
const category = categorySelect.value;
const filtered = easings.filter(e =>
category === 'all' || e.category === category
);
container.innerHTML = '';
filtered.forEach((easing, index) => {
const card = document.createElement('div');
card.className = 'easing-card';
card.innerHTML = `
<div class="easing-name">
<span>${easing.name}</span>
<button class="copy-btn" data-ease="${easing.name}">Copy</button>
</div>
<div class="track" data-index="${index}">
<div class="box"></div>
</div>
<div class="curve-preview" style="${showCurves ? '' : 'display: none;'}">
<svg width="100%" height="100%" viewBox="0 0 100 80" preserveAspectRatio="none">
<path class="curve-line" d="M 5 75 ${generateCurvePath(easing.name)}" />
</svg>
</div>
<div class="use-case">
<strong>Use case:</strong> ${easing.useCase}
</div>
`;
container.appendChild(card);
// Add copy functionality
card.querySelector('.copy-btn').addEventListener('click', () => {
copyToClipboard(easing.name);
});
// Add click to animate
card.querySelector('.track').addEventListener('click', () => {
animateBox(index, easing.name);
});
});
}
// Generate approximate curve path for visualization
function generateCurvePath(easeName) {
// Simplified curve representations
const curves = {
"none": "L 95 5",
"power1.out": "Q 50 40, 95 5",
"power2.out": "Q 50 50, 95 5",
"power3.out": "Q 50 60, 95 5",
"power4.out": "Q 50 65, 95 5",
"power1.in": "Q 50 40, 95 75",
"power2.in": "Q 50 30, 95 75",
"back.out(1.7)": "Q 50 -10, 95 5",
"back.out(2.5)": "Q 50 -20, 95 5",
"back.in(1.7)": "Q 50 90, 95 75",
"back.inOut(1.7)": "Q 25 -10, 50 40 T 95 5",
"elastic.out(1, 0.3)": "Q 30 -5, 50 45 T 70 5 T 95 5",
"bounce.out": "L 40 30 L 60 10 L 75 5 L 95 5",
"circ.out": "Q 30 50, 95 5",
"expo.out": "Q 20 60, 95 5",
"sine.out": "Q 50 35, 95 5",
};
return curves[easeName] || "L 95 5";
}
// Animate individual box
function animateBox(index, easeName) {
const track = document.querySelector(`.track[data-index="${index}"]`);
const box = track.querySelector('.box');
gsap.fromTo(box,
{ x: 0 },
{
x: distance,
duration: duration,
ease: easeName,
onComplete: () => {
gsap.to(box, { x: 0, duration: 0.3, ease: "power1.in" });
}
}
);
}
// Play all animations
function playAll() {
const tracks = document.querySelectorAll('.track');
tracks.forEach((track, index) => {
const box = track.querySelector('.box');
const easeName = easings[index].name;
gsap.fromTo(box,
{ x: 0 },
{
x: distance,
duration: duration,
ease: easeName,
delay: index * 0.05,
onComplete: () => {
gsap.to(box, { x: 0, duration: 0.3, ease: "power1.in" });
}
}
);
});
}
// Reset all animations
function resetAll() {
const boxes = document.querySelectorAll('.box');
gsap.killTweensOf(boxes);
gsap.set(boxes, { x: 0 });
}
// Copy to clipboard
function copyToClipboard(text) {
navigator.clipboard.writeText(`ease: "${text}"`).then(() => {
showToast();
});
}
function showToast() {
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 2000);
}
// Event listeners
durationInput.addEventListener('input', (e) => {
duration = parseFloat(e.target.value);
});
distanceInput.addEventListener('input', (e) => {
distance = parseInt(e.target.value);
});
categorySelect.addEventListener('change', renderEasings);
playAllBtn.addEventListener('click', playAll);
resetAllBtn.addEventListener('click', resetAll);
toggleCurvesBtn.addEventListener('click', () => {
showCurves = !showCurves;
renderEasings();
});
// Initial render
renderEasings();
</script>
</body>
</html>
GSAP ScrollTrigger Examples
Real-world patterns and implementations for common ScrollTrigger use cases.
📚 Available Examples
Core Patterns
All examples are self-contained HTML files demonstrating specific ScrollTrigger patterns. Open any file directly in your browser to see the demo.
---
🎯 Pattern Categories
1. Fade & Entrance Animations
Use Cases: Content reveals, card grids, feature sections
// Basic fade in
gsap.from(".element", {
opacity: 0,
y: 50,
scrollTrigger: {
trigger: ".element",
start: "top 80%"
}
});Variations:
- Batch animations for multiple elements
- Stagger from different directions (center, edges, random)
- Grid-based stagger patterns
---
2. Pinning Animations
Use Cases: Storytelling, feature highlights, step-by-step guides
ScrollTrigger.create({
trigger: ".section",
start: "top top",
end: "+=1000",
pin: true,
scrub: 1
});Variations:
- Pin with timeline animations
- Multiple pinned sections (stacked cards)
- Pin with snap points
---
3. Horizontal Scrolling
Use Cases: Portfolios, product showcases, timelines
gsap.to(".panels", {
xPercent: -100 * (panels.length - 1),
ease: "none",
scrollTrigger: {
trigger: ".container",
pin: true,
scrub: 1,
end: () => "+=" + container.offsetWidth
}
});Variations:
- With snap points between panels
- Nested animations within panels
- Progress indicators
---
4. Parallax Effects
Use Cases: Hero sections, immersive storytelling, depth perception
gsap.to(".bg", {
y: 200,
ease: "none",
scrollTrigger: {
trigger: ".section",
start: "top bottom",
end: "bottom top",
scrub: true
}
});Variations:
- Multi-layer parallax (2-5 layers)
- Parallax with zoom
- Directional parallax (horizontal/vertical)
---
5. Text Animations
Use Cases: Headlines, storytelling, attention-grabbing reveals
// Requires SplitText plugin (Club GreenSock)
const split = new SplitText(".text", { type: "lines" });
gsap.from(split.lines, {
opacity: 0,
y: 20,
stagger: 0.1,
scrollTrigger: {
trigger: ".text",
start: "top 80%"
}
});Variations:
- Line-by-line reveals
- Character animations
- Typewriter effects
- Text masking
---
6. Image Reveals
Use Cases: Galleries, case studies, portfolio pieces
// Curtain reveal
gsap.to(".curtain", {
xPercent: 100,
duration: 1,
scrollTrigger: {
trigger: ".image",
start: "top 80%"
}
});Variations:
- Clip-path reveals
- Scale reveals
- Directional wipes
- Parallax zoom
---
🚀 Quick Start
Run Any Example
1. Open any .html file in your browser 2. Or serve with a local server:
python -m http.server 8000
# Navigate to http://localhost:8000/pin_animation.htmlIntegrate into Your Project
Each example is self-contained. Copy the relevant code sections:
1. HTML Structure - Copy the markup 2. CSS Styles - Copy relevant styles 3. JavaScript - Copy animation code, adjust selectors
---
📖 Learning Path
Beginner
1. Study basic fade-in patterns 2. Experiment with stagger animations 3. Try different easing functions 4. Enable debug markers to visualize triggers
Intermediate
1. Build pinned sections with timelines 2. Create parallax effects 3. Implement horizontal scrolling 4. Add scroll progress indicators
Advanced
1. Create image sequence animations 2. Build complex multi-section experiences 3. Integrate with Three.js or Canvas 4. Implement custom ScrollTrigger behaviors
---
💡 Best Practices
Performance
1. Use transform and opacity (GPU-accelerated) 2. Apply will-change to animated elements 3. Use scrub for smooth scroll-driven animations 4. Limit number of active ScrollTriggers
UX
1. Keep animations subtle and purposeful 2. Test on multiple devices and screen sizes 3. Provide fallbacks for users with motion sensitivity 4. Ensure content is accessible without animations
Development
1. Enable debug markers during development 2. Use meaningful trigger/element names 3. Comment complex animation sequences 4. Test scroll performance with DevTools
---
🔗 Additional Resources
Official GSAP Resources
Included in This Package
../references/api_reference.md- Complete GSAP & ScrollTrigger API../references/easing_guide.md- Visual easing reference../references/common_patterns.md- Copy-paste pattern library../easings/easing_visualizer.html- Interactive easing tool../starter_scroll/- Complete website template
---
🎨 Customization Tips
Adjust Timing
// Slower animation
duration: 2 // instead of 1
// More gradual scroll response
scrub: 2 // instead of true or 1Change Trigger Points
// Trigger earlier
start: "top 90%" // instead of "top 80%"
// Trigger later
start: "top 50%"Modify Easing
// More dramatic
ease: "power3.out" // instead of "power2.out"
// Playful
ease: "back.out(1.7)"---
🐛 Common Issues
Animation Not Triggering
- Check element selectors
- Enable debug markers:
markers: true - Verify trigger element exists in DOM
- Check browser console for errors
Jumpy Animations
- Use
scrubfor smooth scroll-tied animations - Add
anticipatePin: 1for pinned sections - Check for layout shifts (images without dimensions)
Poor Performance
- Reduce number of animated elements
- Use
ScrollTrigger.batch()for many elements - Avoid animating expensive properties (width, height)
- Test on target devices
---
📱 Mobile Considerations
// Disable complex animations on mobile
if (window.innerWidth < 768) {
// Use simpler animations or CSS-only
}
// Or use ScrollTrigger.matchMedia()
ScrollTrigger.matchMedia({
"(min-width: 800px)": function() {
// Desktop animations
},
"(max-width: 799px)": function() {
// Mobile animations
}
});---
🎯 Example Use Cases
E-commerce
- Product reveals on scroll
- Feature highlights with pinning
- Parallax hero sections
- Image galleries with stagger
Portfolios
- Horizontal project showcases
- Case study storytelling with pins
- Image sequence animations
- Smooth page transitions
Marketing Sites
- Feature sections with animations
- Testimonial sliders
- Pricing table reveals
- CTA animations
Storytelling
- Chapter-based pinned sections
- Text reveals for narrative
- Image sequences for progression
- Parallax depth for immersion
---
📝 Code Snippets
Quick Copy-Paste Examples
Fade In on Scroll
<div class="fade-in">Content here</div>
<script>
gsap.from(".fade-in", {
opacity: 0,
y: 50,
duration: 1,
scrollTrigger: {
trigger: ".fade-in",
start: "top 80%"
}
});
</script>Progress Bar
<div class="progress" style="position: fixed; top: 0; left: 0; height: 4px; background: blue; transform-origin: left;"></div>
<script>
gsap.to(".progress", {
scaleX: 1,
ease: "none",
scrollTrigger: {
start: "top top",
end: "bottom bottom",
scrub: 0.3
}
});
</script>Smooth Scroll Navigation
<a href="#section2">Go to Section 2</a>
<script>
gsap.registerPlugin(ScrollToPlugin);
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function(e) {
e.preventDefault();
gsap.to(window, {
duration: 1,
scrollTo: this.getAttribute("href"),
ease: "power2.inOut"
});
});
});
</script>---
🎓 Next Steps
1. Explore - Open examples and study the code 2. Modify - Change values and see what happens 3. Combine - Mix patterns to create unique effects 4. Build - Create your own scroll-driven experience
---
Need Help?
- Check the API Reference
- Try the Easing Visualizer
- Browse Common Patterns
- Visit GSAP Forums
---
Happy Scrolling! 🎉
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GSAP ScrollTrigger Starter</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<!-- Loading Screen -->
<div id="loading">
<div class="spinner"></div>
<p>Loading...</p>
</div>
<!-- Navigation -->
<nav class="nav">
<div class="nav-container">
<div class="logo">ScrollSite</div>
<ul class="nav-links">
<li><a href="#hero" class="nav-item">Home</a></li>
<li><a href="#features" class="nav-item">Features</a></li>
<li><a href="#gallery" class="nav-item">Gallery</a></li>
<li><a href="#contact" class="nav-item">Contact</a></li>
</ul>
</div>
</nav>
<!-- Progress Bar -->
<div class="progress-bar"></div>
<!-- Hero Section -->
<section id="hero" class="hero">
<div class="hero-bg"></div>
<div class="hero-content">
<h1 class="hero-title">Welcome to ScrollSite</h1>
<p class="hero-subtitle">Experience smooth scroll-driven animations</p>
<button class="cta-btn">Get Started</button>
</div>
<div class="scroll-indicator">
<span>Scroll to explore</span>
<div class="mouse"></div>
</div>
</section>
<!-- Features Section (Fade In) -->
<section id="features" class="section features-section">
<div class="container">
<h2 class="section-title">Features</h2>
<div class="cards-grid">
<div class="card fade-in">
<div class="card-icon">🚀</div>
<h3>Fast Performance</h3>
<p>Optimized animations with GPU acceleration for smooth 60fps experiences.</p>
</div>
<div class="card fade-in">
<div class="card-icon">🎨</div>
<h3>Beautiful Design</h3>
<p>Modern, responsive layouts that look stunning on all devices.</p>
</div>
<div class="card fade-in">
<div class="card-icon">⚡</div>
<h3>Easy to Use</h3>
<p>Simple API with powerful features for creating complex animations.</p>
</div>
<div class="card fade-in">
<div class="card-icon">📱</div>
<h3>Responsive</h3>
<p>Adapts seamlessly to desktop, tablet, and mobile screens.</p>
</div>
<div class="card fade-in">
<div class="card-icon">🔧</div>
<h3>Customizable</h3>
<p>Easily modify animations, timing, and easing functions.</p>
</div>
<div class="card fade-in">
<div class="card-icon">💎</div>
<h3>Production Ready</h3>
<p>Battle-tested code ready for real-world projects.</p>
</div>
</div>
</div>
</section>
<!-- Parallax Section -->
<section class="section parallax-section">
<div class="parallax-bg"></div>
<div class="parallax-content container">
<h2 class="section-title">Parallax Effects</h2>
<p class="section-text">Background moves at a different speed for depth perception</p>
</div>
</section>
<!-- Pin Section -->
<section class="section pin-section">
<div class="container pin-container">
<div class="pin-content">
<h2 class="pin-title">Pinned Animation</h2>
<p class="pin-text">This section stays pinned while content animates</p>
<div class="pin-boxes">
<div class="pin-box box-1"></div>
<div class="pin-box box-2"></div>
<div class="pin-box box-3"></div>
</div>
</div>
</div>
</section>
<!-- Image Gallery (Stagger) -->
<section id="gallery" class="section gallery-section">
<div class="container">
<h2 class="section-title">Gallery</h2>
<div class="gallery-grid">
<div class="gallery-item">
<div class="gallery-image" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);"></div>
<div class="gallery-overlay">
<h3>Project 1</h3>
<p>Creative Design</p>
</div>
</div>
<div class="gallery-item">
<div class="gallery-image" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);"></div>
<div class="gallery-overlay">
<h3>Project 2</h3>
<p>Brand Identity</p>
</div>
</div>
<div class="gallery-item">
<div class="gallery-image" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);"></div>
<div class="gallery-overlay">
<h3>Project 3</h3>
<p>Web Development</p>
</div>
</div>
<div class="gallery-item">
<div class="gallery-image" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);"></div>
<div class="gallery-overlay">
<h3>Project 4</h3>
<p>Mobile App</p>
</div>
</div>
<div class="gallery-item">
<div class="gallery-image" style="background: linear-gradient(135deg, #fa709a 0%, #fee140 100%);"></div>
<div class="gallery-overlay">
<h3>Project 5</h3>
<p>UI/UX Design</p>
</div>
</div>
<div class="gallery-item">
<div class="gallery-image" style="background: linear-gradient(135deg, #30cfd0 0%, #330867 100%);"></div>
<div class="gallery-overlay">
<h3>Project 6</h3>
<p>Motion Graphics</p>
</div>
</div>
</div>
</div>
</section>
<!-- Horizontal Scroll Section -->
<section class="section horizontal-section">
<div class="horizontal-wrapper">
<div class="horizontal-panels">
<div class="horizontal-panel" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<h2>Panel 1</h2>
<p>Horizontal scrolling creates unique experiences</p>
</div>
<div class="horizontal-panel" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<h2>Panel 2</h2>
<p>Each panel can have different content</p>
</div>
<div class="horizontal-panel" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">
<h2>Panel 3</h2>
<p>Scroll vertically to move horizontally</p>
</div>
<div class="horizontal-panel" style="background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);">
<h2>Panel 4</h2>
<p>Great for storytelling and portfolios</p>
</div>
</div>
</div>
</section>
<!-- Text Reveal Section -->
<section class="section text-section">
<div class="container">
<div class="text-reveal">
<h2 class="reveal-line">Text reveals</h2>
<h2 class="reveal-line">line by line</h2>
<h2 class="reveal-line">for dramatic</h2>
<h2 class="reveal-line">storytelling</h2>
</div>
</div>
</section>
<!-- Contact Section -->
<section id="contact" class="section contact-section">
<div class="container">
<h2 class="section-title">Get in Touch</h2>
<form class="contact-form">
<div class="form-group fade-in">
<input type="text" placeholder="Your Name" required>
</div>
<div class="form-group fade-in">
<input type="email" placeholder="Your Email" required>
</div>
<div class="form-group fade-in">
<textarea placeholder="Your Message" rows="5" required></textarea>
</div>
<button type="submit" class="submit-btn fade-in">Send Message</button>
</form>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="container">
<p>© 2025 ScrollSite. Built with GSAP & ScrollTrigger.</p>
<p class="footer-links">
<a href="#">GitHub</a> •
<a href="#">Twitter</a> •
<a href="#">LinkedIn</a>
</p>
</div>
</footer>
<!-- GSAP Scripts -->
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/ScrollToPlugin.min.js"></script>
<script src="main.js"></script>
</body>
</html>
// Register GSAP plugins
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin);
// ============================
// Loading Screen
// ============================
window.addEventListener("load", () => {
const loading = document.getElementById("loading");
gsap.to(loading, {
opacity: 0,
duration: 0.5,
delay: 0.5,
onComplete: () => {
loading.classList.add("hidden");
initAnimations();
}
});
});
// ============================
// Initialize All Animations
// ============================
function initAnimations() {
// Refresh ScrollTrigger after initialization
ScrollTrigger.refresh();
// Progress Bar
setupProgressBar();
// Navigation
setupNavigation();
// Hero Section
animateHero();
// Features Section (Fade In Cards)
animateFeatures();
// Parallax Section
setupParallax();
// Pin Section
setupPinSection();
// Gallery Section (Stagger)
animateGallery();
// Horizontal Scroll Section
setupHorizontalScroll();
// Text Reveal Section
animateTextReveal();
// Contact Form
animateContactForm();
// Smooth Scroll Links
setupSmoothScroll();
// Active Nav Tracking
trackActiveSection();
}
// ============================
// Progress Bar
// ============================
function setupProgressBar() {
gsap.to(".progress-bar", {
scaleX: 1,
ease: "none",
scrollTrigger: {
start: "top top",
end: "bottom bottom",
scrub: 0.3
}
});
}
// ============================
// Navigation Hide/Show
// ============================
function setupNavigation() {
let lastScroll = 0;
const nav = document.querySelector(".nav");
ScrollTrigger.create({
start: 100,
end: 99999,
onUpdate: (self) => {
const currentScroll = self.scroll();
if (currentScroll > lastScroll && currentScroll > 100) {
// Scrolling down - hide nav
gsap.to(nav, { y: -100, duration: 0.3, ease: "power2.out" });
} else {
// Scrolling up - show nav
gsap.to(nav, { y: 0, duration: 0.3, ease: "power2.out" });
}
lastScroll = currentScroll;
}
});
}
// ============================
// Hero Section
// ============================
function animateHero() {
const tl = gsap.timeline();
tl.from(".hero-title", {
opacity: 0,
y: 100,
duration: 1,
ease: "power3.out"
})
.from(".hero-subtitle", {
opacity: 0,
y: 50,
duration: 0.8,
ease: "power2.out"
}, "-=0.5")
.from(".cta-btn", {
scale: 0,
duration: 0.5,
ease: "back.out(1.7)"
}, "-=0.3")
.from(".scroll-indicator", {
opacity: 0,
y: 30,
duration: 0.8,
ease: "power2.out"
}, "-=0.3");
// Parallax effect on hero background
gsap.to(".hero-bg", {
yPercent: 50,
ease: "none",
scrollTrigger: {
trigger: ".hero",
start: "top top",
end: "bottom top",
scrub: true
}
});
// Fade out hero content on scroll
gsap.to(".hero-content", {
opacity: 0,
y: 100,
ease: "none",
scrollTrigger: {
trigger: ".hero",
start: "top top",
end: "bottom top",
scrub: true
}
});
}
// ============================
// Features Section (Cards)
// ============================
function animateFeatures() {
gsap.from(".card", {
opacity: 0,
y: 80,
duration: 0.8,
ease: "power2.out",
stagger: {
each: 0.15,
from: "start"
},
scrollTrigger: {
trigger: ".features-section",
start: "top 80%"
}
});
}
// ============================
// Parallax Section
// ============================
function setupParallax() {
gsap.to(".parallax-bg", {
y: 200,
ease: "none",
scrollTrigger: {
trigger: ".parallax-section",
start: "top bottom",
end: "bottom top",
scrub: true
}
});
gsap.from(".parallax-content", {
opacity: 0,
scale: 0.8,
scrollTrigger: {
trigger: ".parallax-section",
start: "top 80%",
end: "top 50%",
scrub: true
}
});
}
// ============================
// Pin Section with Animation
// ============================
function setupPinSection() {
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".pin-section",
start: "top top",
end: "+=1000",
pin: true,
scrub: 1,
anticipatePin: 1
}
});
tl.from(".pin-title", { opacity: 0, y: -50 })
.from(".pin-text", { opacity: 0, y: -30 }, "-=0.5")
.from(".pin-box", {
scale: 0,
rotation: 180,
stagger: 0.2,
ease: "back.out(1.7)"
})
.to(".pin-box", {
y: -50,
stagger: {
each: 0.1,
yoyo: true,
repeat: 1
}
});
}
// ============================
// Gallery Section (Stagger)
// ============================
function animateGallery() {
gsap.from(".gallery-item", {
opacity: 0,
scale: 0.5,
duration: 0.8,
ease: "back.out(1.7)",
stagger: {
each: 0.15,
from: "center",
grid: "auto"
},
scrollTrigger: {
trigger: ".gallery-section",
start: "top 70%"
}
});
}
// ============================
// Horizontal Scroll Section
// ============================
function setupHorizontalScroll() {
const panels = gsap.utils.toArray(".horizontal-panel");
const scrollTween = gsap.to(panels, {
xPercent: -100 * (panels.length - 1),
ease: "none",
scrollTrigger: {
trigger: ".horizontal-wrapper",
pin: true,
scrub: 1,
snap: {
snapTo: 1 / (panels.length - 1),
duration: { min: 0.2, max: 0.5 },
ease: "power1.inOut"
},
end: () => "+=" + (document.querySelector(".horizontal-wrapper").offsetWidth * panels.length)
}
});
// Animate content within each panel
panels.forEach((panel, i) => {
gsap.from(panel.children, {
opacity: 0,
y: 50,
scrollTrigger: {
trigger: panel,
containerAnimation: scrollTween,
start: "left center",
end: "center center",
scrub: true
}
});
});
}
// ============================
// Text Reveal Section
// ============================
function animateTextReveal() {
gsap.from(".reveal-line", {
opacity: 0,
y: 100,
duration: 1,
ease: "power3.out",
stagger: 0.2,
scrollTrigger: {
trigger: ".text-section",
start: "top 70%"
}
});
}
// ============================
// Contact Form
// ============================
function animateContactForm() {
gsap.from(".contact-form .fade-in", {
opacity: 0,
y: 50,
duration: 0.8,
ease: "power2.out",
stagger: 0.15,
scrollTrigger: {
trigger: ".contact-form",
start: "top 80%"
}
});
// Form submission animation (demo)
const form = document.querySelector(".contact-form");
const submitBtn = document.querySelector(".submit-btn");
form.addEventListener("submit", (e) => {
e.preventDefault();
gsap.to(submitBtn, {
scale: 0.9,
duration: 0.1,
yoyo: true,
repeat: 1,
onComplete: () => {
submitBtn.textContent = "✓ Message Sent!";
submitBtn.style.background = "#4caf50";
setTimeout(() => {
submitBtn.textContent = "Send Message";
submitBtn.style.background = "";
form.reset();
}, 3000);
}
});
});
}
// ============================
// Smooth Scroll to Sections
// ============================
function setupSmoothScroll() {
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute("href"));
if (target) {
gsap.to(window, {
duration: 1,
scrollTo: {
y: target,
offsetY: 80 // Account for fixed header
},
ease: "power2.inOut"
});
}
});
});
}
// ============================
// Active Navigation Tracking
// ============================
function trackActiveSection() {
const sections = gsap.utils.toArray("section[id]");
const navItems = gsap.utils.toArray(".nav-item");
sections.forEach((section, i) => {
ScrollTrigger.create({
trigger: section,
start: "top center",
end: "bottom center",
onEnter: () => setActiveNav(section.id),
onEnterBack: () => setActiveNav(section.id)
});
});
function setActiveNav(id) {
navItems.forEach(item => {
if (item.getAttribute("href") === `#${id}`) {
item.classList.add("active");
} else {
item.classList.remove("active");
}
});
}
}
// ============================
// Responsive Handling
// ============================
// Disable complex animations on mobile for performance
if (window.innerWidth < 768) {
ScrollTrigger.config({
limitCallbacks: true,
ignoreMobileResize: true
});
}
// ============================
// Refresh on Resize
// ============================
let resizeTimer;
window.addEventListener("resize", () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
ScrollTrigger.refresh();
}, 250);
});
// ============================
// Performance Optimizations
// ============================
// Add will-change to animated elements
gsap.set(".fade-in, .card, .gallery-item, .pin-box", {
willChange: "transform, opacity"
});
// Debug mode (set to false in production)
const DEBUG = false;
if (DEBUG) {
ScrollTrigger.defaults({
markers: true
});
}
// ============================
// Console Welcome Message
// ============================
console.log(
"%c🚀 GSAP ScrollTrigger Starter",
"font-size: 20px; font-weight: bold; color: #667eea;"
);
console.log(
"%cBuilt with GSAP 3 & ScrollTrigger",
"font-size: 14px; color: #764ba2;"
);
GSAP ScrollTrigger Starter Template
A production-ready, scroll-driven website template featuring modern GSAP & ScrollTrigger animations.
✨ Features
Animations Included
- ✅ Hero Section - Parallax background with fade-out content
- ✅ Fade-in Cards - Staggered entrance animations
- ✅ Parallax Section - Multi-layer depth effect
- ✅ Pin Section - Content pinned while animations play
- ✅ Stagger Gallery - Grid animation from center
- ✅ Horizontal Scroll - Vertical scroll triggering horizontal movement
- ✅ Text Reveal - Line-by-line text animations
- ✅ Progress Bar - Scroll progress indicator
- ✅ Smart Navigation - Hide on scroll down, show on scroll up
- ✅ Active Nav Tracking - Highlights current section
- ✅ Smooth Scroll - Click anchors for smooth navigation
- ✅ Responsive - Mobile-optimized animations
- ✅ Loading Screen - Smooth intro animation
Technical Features
- 🚀 Performance Optimized - GPU-accelerated animations, will-change optimization
- 📱 Mobile Responsive - Adapts animations for smaller screens
- 🎨 Modern Design - Clean, gradient-based aesthetic
- ⚡ Zero Dependencies - Only GSAP required (via CDN)
- 🛠 Easy Customization - Well-organized, commented code
- 📦 Production Ready - Tested and optimized for real-world use
---
🚀 Quick Start
Option 1: Local Development
# Serve with any static server
python -m http.server 8000
# Or
npx serve
# Or
php -S localhost:8000Open http://localhost:8000 in your browser.
Option 2: Direct Open
Simply open index.html in your browser (some animations may require a server for full functionality).
---
📁 Project Structure
starter_scroll/
├── index.html # Main HTML structure
├── style.css # Complete styles (~580 lines)
├── main.js # GSAP animations (~420 lines)
└── README.md # This file---
🎨 Customization Guide
1. Change Colors
Edit CSS variables in style.css:
:root {
--primary: #667eea; /* Primary gradient color */
--secondary: #764ba2; /* Secondary gradient color */
--text-dark: #333; /* Main text color */
--text-light: #666; /* Secondary text color */
--bg-light: #f9f9f9; /* Light background */
}2. Adjust Animation Timing
In main.js, modify animation durations and easing:
// Example: Hero animation
tl.from(".hero-title", {
opacity: 0,
y: 100,
duration: 1, // Change duration
ease: "power3.out" // Change easing
});Common Easing Options:
power2.out- Standard (most common)power3.out- Dramaticback.out(1.7)- Playful overshootelastic.out- Bouncynone- Linear (for scrubbed animations)
3. Modify Scroll Trigger Points
Adjust start and end positions:
scrollTrigger: {
trigger: ".section",
start: "top 80%", // When top of section hits 80% of viewport
end: "bottom 20%", // When bottom hits 20% of viewport
scrub: true // Tie animation to scroll
}Position Format: "[trigger position] [viewport position]"
- Examples:
"top top","center center","bottom 80%"
4. Enable Debug Markers
In main.js, set DEBUG = true:
const DEBUG = true; // Shows visual scroll trigger markersThis displays colored markers showing trigger start/end points.
5. Add Your Own Sections
HTML:
<section class="section my-section">
<div class="container">
<h2 class="section-title">My Section</h2>
<p class="fade-in">Content here...</p>
</div>
</section>JavaScript:
gsap.from(".my-section .fade-in", {
opacity: 0,
y: 50,
duration: 1,
scrollTrigger: {
trigger: ".my-section",
start: "top 80%"
}
});---
🎯 Animation Patterns Reference
Fade In on Scroll
gsap.from(".element", {
opacity: 0,
y: 50,
duration: 1,
scrollTrigger: {
trigger: ".element",
start: "top 80%"
}
});Scroll-Driven Animation (Scrubbed)
gsap.to(".element", {
x: 500,
scrollTrigger: {
trigger: ".container",
start: "top top",
end: "bottom top",
scrub: true // Animation tied to scroll position
}
});Pin Section
ScrollTrigger.create({
trigger: ".section",
start: "top top",
end: "+=1000", // Pin for 1000px of scrolling
pin: true,
pinSpacing: true
});Stagger Animation
gsap.from(".item", {
opacity: 0,
y: 50,
stagger: 0.15, // 0.15s delay between each
scrollTrigger: {
trigger: ".container",
start: "top 80%"
}
});Parallax Effect
gsap.to(".bg", {
y: 200,
ease: "none",
scrollTrigger: {
trigger: ".section",
start: "top bottom",
end: "bottom top",
scrub: true
}
});---
📱 Responsive Behavior
Animations automatically adapt for mobile devices:
- Reduced animation complexity on
< 768pxscreens limitCallbacksenabled for better mobile performance- Navigation adapts to touch interactions
- Horizontal scroll becomes vertical scroll on mobile
Manual Responsive Control:
ScrollTrigger.matchMedia({
// Desktop
"(min-width: 800px)": function() {
// Desktop-only animations
},
// Mobile
"(max-width: 799px)": function() {
// Mobile-only animations
}
});---
⚡ Performance Tips
Already Implemented
1. GPU Acceleration - Using transform and opacity (GPU-friendly properties) 2. Will-change - Applied to animated elements 3. Efficient Selectors - Cached element queries 4. Debounced Resize - Prevents excessive refresh calls 5. Mobile Optimization - Simplified animations on small screens
Additional Optimizations
1. Reduce Animation Count
// Instead of animating many elements
gsap.from(".item", { ... });
// Use batch for better performance
ScrollTrigger.batch(".item", {
onEnter: batch => gsap.from(batch, { ... })
});2. Lower Scrub Precision
scrollTrigger: {
scrub: 1 // Instead of true (smoother but less precise)
}3. Disable Markers in Production
const DEBUG = false; // Removes visual markers---
🐛 Troubleshooting
Animations Not Triggering
1. Check console for errors 2. Enable debug markers: DEBUG = true 3. Verify element selectors match HTML 4. Ensure content is loaded: animations run after window.load
Jumpy Scroll on Mobile
// Add to ScrollTrigger config
ScrollTrigger.config({
ignoreMobileResize: true
});Layout Shifts After Pin
ScrollTrigger.create({
pin: true,
pinSpacing: true // or false, depending on desired behavior
});Horizontal Scroll Issues
1. Check container width calculation 2. Verify number of panels 3. Test on different screen sizes
---
🔧 Advanced Customization
Add Timeline Sequences
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".section",
start: "top 80%"
}
});
tl.from(".title", { opacity: 0, y: -50 })
.from(".subtitle", { opacity: 0, y: -30 }, "-=0.3")
.from(".button", { scale: 0 }, "-=0.2");Callback Functions
scrollTrigger: {
trigger: ".section",
start: "top 80%",
onEnter: () => console.log("Entered"),
onLeave: () => console.log("Left"),
onEnterBack: () => console.log("Scrolled back"),
onLeaveBack: () => console.log("Left backwards")
}Toggle Actions
scrollTrigger: {
toggleActions: "play pause resume reset"
// Format: onEnter onLeave onEnterBack onLeaveBack
// Options: play, pause, resume, reset, restart, complete, reverse, none
}---
📚 Resources
- GSAP Documentation
- ScrollTrigger Docs
- Easing Visualizer - Included in this package
- GSAP Forums
- CodePen GSAP Examples
---
🎓 Learning Path
Beginner
1. Study main.js - Understand each animation function 2. Enable debug markers to visualize trigger points 3. Modify existing animations (timing, easing, distances) 4. Add simple fade-in animations to new elements
Intermediate
1. Create custom timeline sequences 2. Build your own pinned sections 3. Implement custom scroll progress indicators 4. Add toggle actions and callbacks
Advanced
1. Create image sequence animations 2. Build complex horizontal scroll experiences 3. Integrate with Three.js or Canvas 4. Implement custom easing functions
---
📄 Browser Compatibility
- ✅ Chrome/Edge 90+ (Excellent)
- ✅ Firefox 88+ (Excellent)
- ✅ Safari 14+ (Good)
- ⚠️ IE11 (Not supported - requires polyfills)
---
🚢 Deployment
Production Checklist
1. ✅ Set DEBUG = false in main.js 2. ✅ Remove console logs (or keep welcome message) 3. ✅ Test on multiple devices and browsers 4. ✅ Optimize images and assets 5. ✅ Consider self-hosting GSAP (instead of CDN) 6. ✅ Minify CSS and JS for production 7. ✅ Test scroll performance with DevTools
Hosting
Works with any static hosting:
- Netlify - Drop folder, auto-deploy
- Vercel - Git integration, instant deploys
- GitHub Pages - Free hosting from repo
- Cloudflare Pages - Fast global CDN
---
📝 License
This template is provided as-is for use in any project (personal or commercial).
GSAP is licensed under GreenSock License. Free for most projects, paid license required for commercial use.
---
🙏 Credits
Built with:
- GSAP - Animation library
- ScrollTrigger - Scroll-driven animations
- ScrollToPlugin - Smooth scroll navigation
---
💡 Tips for Success
1. Start Simple - Add animations incrementally, test frequently 2. Use Debug Markers - Visual feedback makes learning easier 3. Study Examples - Check GSAP CodePens for inspiration 4. Test on Mobile - Performance can vary significantly 5. Read the Docs - ScrollTrigger has many powerful options 6. Join Community - GSAP forums are helpful and active
---
🎉 What's Next?
- Add your own content and branding
- Customize colors and typography
- Experiment with different easing functions
- Try the Easing Visualizer
- Explore advanced patterns in
common_patterns.md - Build something amazing! 🚀
---
Happy Animating! ⚡
/* ===========================
Reset & Base Styles
=========================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #667eea;
--secondary: #764ba2;
--text-dark: #333;
--text-light: #666;
--bg-light: #f9f9f9;
--white: #ffffff;
--shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 20px 60px rgba(0, 0, 0, 0.15);
}
html {
scroll-behavior: smooth;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: var(--text-dark);
overflow-x: hidden;
background: var(--white);
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 40px;
}
section {
position: relative;
}
.section {
padding: 100px 0;
}
.section-title {
font-size: 3rem;
font-weight: 700;
text-align: center;
margin-bottom: 60px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.section-text {
font-size: 1.2rem;
text-align: center;
color: var(--text-light);
max-width: 600px;
margin: 0 auto;
}
/* ===========================
Loading Screen
=========================== */
#loading {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, var(--primary), var(--secondary));
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
z-index: 9999;
transition: opacity 0.5s, visibility 0.5s;
}
#loading.hidden {
opacity: 0;
visibility: hidden;
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
#loading p {
color: white;
margin-top: 20px;
font-size: 1.2rem;
}
/* ===========================
Navigation
=========================== */
.nav {
position: fixed;
top: 0;
left: 0;
width: 100%;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
box-shadow: var(--shadow);
z-index: 1000;
transition: transform 0.3s;
}
.nav-container {
max-width: 1200px;
margin: 0 auto;
padding: 20px 40px;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.nav-links {
display: flex;
gap: 30px;
list-style: none;
}
.nav-item {
color: var(--text-dark);
text-decoration: none;
font-weight: 500;
transition: color 0.3s;
position: relative;
}
.nav-item::after {
content: '';
position: absolute;
bottom: -5px;
left: 0;
width: 0%;
height: 2px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
transition: width 0.3s;
}
.nav-item:hover::after,
.nav-item.active::after {
width: 100%;
}
/* ===========================
Progress Bar
=========================== */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: linear-gradient(90deg, var(--primary), var(--secondary));
transform-origin: left;
transform: scaleX(0);
z-index: 1001;
}
/* ===========================
Hero Section
=========================== */
.hero {
position: relative;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
color: white;
}
.hero-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, var(--primary) 0%, var(--secondary) 100%);
z-index: -1;
}
.hero-content {
text-align: center;
z-index: 1;
}
.hero-title {
font-size: 4rem;
font-weight: 700;
margin-bottom: 20px;
text-shadow: 2px 2px 10px rgba(0, 0, 0, 0.3);
}
.hero-subtitle {
font-size: 1.5rem;
margin-bottom: 40px;
opacity: 0.9;
}
.cta-btn {
padding: 15px 40px;
font-size: 1.1rem;
font-weight: 600;
border: 2px solid white;
background: transparent;
color: white;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s;
}
.cta-btn:hover {
background: white;
color: var(--primary);
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.scroll-indicator {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
text-align: center;
color: white;
opacity: 0.8;
}
.scroll-indicator span {
display: block;
margin-bottom: 10px;
font-size: 0.9rem;
}
.mouse {
width: 25px;
height: 40px;
border: 2px solid white;
border-radius: 15px;
margin: 0 auto;
position: relative;
}
.mouse::before {
content: '';
width: 4px;
height: 10px;
background: white;
position: absolute;
top: 8px;
left: 50%;
transform: translateX(-50%);
border-radius: 2px;
animation: scroll 1.5s infinite;
}
@keyframes scroll {
0% { opacity: 1; transform: translateX(-50%) translateY(0); }
100% { opacity: 0; transform: translateX(-50%) translateY(10px); }
}
/* ===========================
Features Section
=========================== */
.features-section {
background: var(--bg-light);
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
}
.card {
background: white;
padding: 40px;
border-radius: 15px;
box-shadow: var(--shadow);
transition: transform 0.3s, box-shadow 0.3s;
text-align: center;
}
.card:hover {
transform: translateY(-10px);
box-shadow: var(--shadow-lg);
}
.card-icon {
font-size: 3rem;
margin-bottom: 20px;
}
.card h3 {
font-size: 1.5rem;
margin-bottom: 15px;
color: var(--text-dark);
}
.card p {
color: var(--text-light);
line-height: 1.8;
}
/* ===========================
Parallax Section
=========================== */
.parallax-section {
position: relative;
height: 70vh;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
color: white;
}
.parallax-bg {
position: absolute;
top: -50%;
left: 0;
width: 100%;
height: 150%;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="%23667eea" d="M0,96L48,112C96,128,192,160,288,160C384,160,480,128,576,122.7C672,117,768,139,864,149.3C960,160,1056,160,1152,138.7C1248,117,1344,75,1392,53.3L1440,32L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path></svg>') center/cover;
z-index: -1;
}
.parallax-content {
text-align: center;
z-index: 1;
}
/* ===========================
Pin Section
=========================== */
.pin-section {
min-height: 150vh;
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
}
.pin-container {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.pin-content {
text-align: center;
}
.pin-title {
font-size: 3rem;
margin-bottom: 20px;
}
.pin-text {
font-size: 1.3rem;
margin-bottom: 60px;
opacity: 0.9;
}
.pin-boxes {
display: flex;
gap: 30px;
justify-content: center;
}
.pin-box {
width: 100px;
height: 100px;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
/* ===========================
Gallery Section
=========================== */
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 30px;
}
.gallery-item {
position: relative;
height: 300px;
border-radius: 15px;
overflow: hidden;
cursor: pointer;
box-shadow: var(--shadow);
transition: transform 0.3s;
}
.gallery-item:hover {
transform: scale(1.05);
}
.gallery-image {
width: 100%;
height: 100%;
transition: transform 0.3s;
}
.gallery-item:hover .gallery-image {
transform: scale(1.1);
}
.gallery-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s;
color: white;
}
.gallery-item:hover .gallery-overlay {
opacity: 1;
}
.gallery-overlay h3 {
font-size: 2rem;
margin-bottom: 10px;
}
/* ===========================
Horizontal Scroll Section
=========================== */
.horizontal-section {
padding: 0;
background: #000;
overflow: hidden;
}
.horizontal-wrapper {
height: 100vh;
display: flex;
align-items: center;
}
.horizontal-panels {
display: flex;
will-change: transform;
}
.horizontal-panel {
min-width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
text-align: center;
padding: 60px;
}
.horizontal-panel h2 {
font-size: 3rem;
margin-bottom: 20px;
}
.horizontal-panel p {
font-size: 1.3rem;
max-width: 500px;
}
/* ===========================
Text Reveal Section
=========================== */
.text-section {
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.text-reveal {
text-align: center;
}
.reveal-line {
font-size: 4rem;
font-weight: 700;
color: white;
text-transform: uppercase;
line-height: 1.2;
margin: 10px 0;
}
/* ===========================
Contact Section
=========================== */
.contact-section {
background: var(--bg-light);
}
.contact-form {
max-width: 600px;
margin: 0 auto;
}
.form-group {
margin-bottom: 25px;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-family: inherit;
font-size: 1rem;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary);
}
.submit-btn {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
border: none;
border-radius: 10px;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.3s, box-shadow 0.3s;
}
.submit-btn:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(102, 126, 234, 0.4);
}
/* ===========================
Footer
=========================== */
.footer {
background: #222;
color: white;
padding: 40px 0;
text-align: center;
}
.footer-links {
margin-top: 15px;
}
.footer-links a {
color: white;
text-decoration: none;
margin: 0 10px;
transition: opacity 0.3s;
}
.footer-links a:hover {
opacity: 0.7;
}
/* ===========================
Responsive
=========================== */
@media (max-width: 768px) {
.section {
padding: 60px 0;
}
.container {
padding: 0 20px;
}
.hero-title {
font-size: 2.5rem;
}
.hero-subtitle {
font-size: 1.2rem;
}
.section-title {
font-size: 2rem;
}
.nav-links {
gap: 15px;
font-size: 0.9rem;
}
.cards-grid {
grid-template-columns: 1fr;
}
.gallery-grid {
grid-template-columns: 1fr;
}
.horizontal-panel h2 {
font-size: 2rem;
}
.reveal-line {
font-size: 2.5rem;
}
}
/* ===========================
Utility Classes
=========================== */
.fade-in {
opacity: 0;
transform: translateY(50px);
}
.will-change {
will-change: transform, opacity;
}
GSAP & ScrollTrigger API Quick Reference
Core GSAP Methods
gsap.to()
Animate FROM current state TO specified values.
gsap.to(target, {
x: 100,
y: 50,
rotation: 360,
duration: 1,
ease: "power2.inOut",
onComplete: callback
});gsap.from()
Animate TO current state FROM specified values.
gsap.from(target, {
opacity: 0,
y: -50,
duration: 0.8,
ease: "back.out"
});gsap.fromTo()
Define both starting and ending values.
gsap.fromTo(target,
{ opacity: 0, scale: 0.5 }, // FROM
{ opacity: 1, scale: 1, duration: 1 } // TO
);gsap.set()
Immediately set properties (no animation).
gsap.set(target, {
x: 100,
opacity: 0.5
});Timeline Methods
Creating Timelines
const tl = gsap.timeline({
repeat: 2,
repeatDelay: 1,
yoyo: true,
paused: false,
onComplete: callback
});Adding Animations
// Add to end of timeline
tl.to(".box", { x: 100 });
// Add at specific time
tl.to(".box", { y: 50 }, 2); // At 2 seconds
// Add relative to previous
tl.to(".box", { rotation: 360 }, "-=0.5"); // 0.5s before previous ends
tl.to(".box", { scale: 2 }, "+=1"); // 1s after previous ends
// Add at label
tl.addLabel("midpoint")
.to(".box", { opacity: 0 }, "midpoint")
.to(".circle", { x: 200 }, "midpoint+=0.5");Timeline Control
tl.play();
tl.pause();
tl.resume();
tl.reverse();
tl.restart();
tl.seek(2); // Go to 2 seconds
tl.progress(0.5); // Go to 50% through timeline
tl.timeScale(2); // 2x speed
tl.kill(); // Destroy timelineCommon Tween Properties
Animation Control
{
duration: 1, // seconds
delay: 0.5, // seconds before start
repeat: 2, // times to repeat (-1 = infinite)
repeatDelay: 1, // seconds between repeats
yoyo: true, // alternate direction on repeat
ease: "power2.inOut", // easing function
paused: true, // start paused
immediateRender: false, // don't render immediately
overwrite: "auto" // handle conflicting tweens
}Callbacks
{
onStart: () => console.log("Started"),
onUpdate: () => console.log("Updating"),
onComplete: () => console.log("Complete"),
onRepeat: () => console.log("Repeating"),
onReverseComplete: () => console.log("Reversed"),
callbackScope: this // scope for all callbacks
}Easing Functions
Power Easings (Most Common)
ease: "none" // Linear
ease: "power1.in"
ease: "power1.out"
ease: "power1.inOut"
ease: "power2.in"
ease: "power2.out"
ease: "power2.inOut"
ease: "power3.in"
ease: "power3.out"
ease: "power3.inOut"
ease: "power4.in"
ease: "power4.out"
ease: "power4.inOut"Special Easings
ease: "back.in(1.7)" // Overshoot, parameter controls amount
ease: "back.out(1.7)"
ease: "back.inOut(1.7)"
ease: "elastic.in(1, 0.3)" // Elastic bounce
ease: "elastic.out(1, 0.3)"
ease: "elastic.inOut(1, 0.3)"
ease: "bounce.in"
ease: "bounce.out"
ease: "bounce.inOut"
ease: "circ.in" // Circular
ease: "circ.out"
ease: "circ.inOut"
ease: "expo.in" // Exponential
ease: "expo.out"
ease: "expo.inOut"
ease: "sine.in" // Sinusoidal
ease: "sine.out"
ease: "sine.inOut"Stagger Configuration
Simple Stagger
stagger: 0.1 // 0.1s between each elementAdvanced Stagger
stagger: {
each: 0.1, // time between each
from: "center", // "start", "center", "end", "edges", "random", or index number
grid: "auto", // [rows, columns] for grid layouts
ease: "power2.inOut", // easing applied to stagger timing
repeat: 2, // repeat the stagger pattern
yoyo: true, // alternate direction
axis: "y" // "x" or "y" for grid stagger direction
}ScrollTrigger Properties
Core Properties
ScrollTrigger.create({
// Trigger & Viewport
trigger: ".element", // Element or selector
start: "top center", // "[trigger position] [viewport position]"
end: "bottom top",
endTrigger: ".other-element", // Different end trigger
// Scrubbing
scrub: true, // Boolean or number (smoothing seconds)
pin: true, // Pin trigger element
pinSpacing: true, // Add space for pinned element
anticipatePin: 1, // Smooth pinning
// Actions
toggleActions: "play pause resume reset", // onEnter onLeave onEnterBack onLeaveBack
toggleClass: "active", // CSS class to toggle
// Animation
animation: tween, // GSAP animation to control
// Snapping
snap: {
snapTo: "labels", // "labels", 0.1, [0, 0.5, 1], or function
duration: { min: 0.2, max: 3 },
delay: 0.2,
ease: "power1.inOut"
},
// Callbacks
onEnter: callback,
onLeave: callback,
onEnterBack: callback,
onLeaveBack: callback,
onUpdate: (self) => {},
onToggle: (self) => {},
onRefresh: (self) => {},
onScrubComplete: (self) => {},
// Advanced
markers: true, // Show visual markers (debug)
id: "myTrigger", // Unique ID
invalidateOnRefresh: false, // Recalculate values on refresh
once: true, // Only trigger once
horizontal: false, // Horizontal scrolling
scroller: ".container", // Custom scroll container
containerAnimation: timeline, // For horizontal scrolling sections
fastScrollEnd: true // Better performance on fast scroll
});ScrollTrigger Start/End Values
// Format: "[trigger position] [viewport position]"
start: "top top" // Trigger top hits viewport top
start: "top center" // Trigger top hits viewport center
start: "top bottom" // Trigger top hits viewport bottom
start: "center center" // Trigger center hits viewport center
start: "bottom top" // Trigger bottom hits viewport top
// With offsets
start: "top top+=100" // 100px below viewport top
start: "top 80%" // 80% down viewport
start: "top-=50 center" // 50px above trigger top
// Dynamic
start: () => "top " + (window.innerHeight * 0.8)
end: () => "+=" + element.offsetHeightScrollTrigger Methods
Static Methods
// Refresh all ScrollTriggers (after DOM changes)
ScrollTrigger.refresh();
// Get all ScrollTriggers
const triggers = ScrollTrigger.getAll();
// Get by ID
const st = ScrollTrigger.getById("myTrigger");
// Kill all
ScrollTrigger.getAll().forEach(t => t.kill());
// Global configuration
ScrollTrigger.config({
limitCallbacks: true, // Better performance
syncInterval: 15, // Throttle scroll checks (ms)
ignoreMobileResize: true // Don't refresh on mobile resize
});
// Defaults for all ScrollTriggers
ScrollTrigger.defaults({
toggleActions: "play none none reverse",
markers: false
});
// Batch animations
ScrollTrigger.batch(".element", {
onEnter: batch => gsap.to(batch, { opacity: 1, stagger: 0.1 }),
start: "top 80%"
});
// Match media queries
ScrollTrigger.matchMedia({
"(min-width: 800px)": function() {
// Desktop animations
},
"(max-width: 799px)": function() {
// Mobile animations
}
});Instance Methods
const st = ScrollTrigger.create({ /* ... */ });
// Control
st.enable();
st.disable();
st.kill();
st.refresh();
// Query
st.isActive; // Boolean
st.progress; // 0-1
st.direction; // 1 (down) or -1 (up)
st.getVelocity(); // Scroll velocity
// Programmatic scroll
st.scroll(500); // Set scroll position
// Get animation
const tween = st.animation; // Associated animation
const scrubTween = st.getTween(); // Scrub tweenUtility Methods
gsap.utils
// Array helpers
gsap.utils.toArray(".class"); // Convert to array
gsap.utils.shuffle(array); // Randomize array order
// Math helpers
gsap.utils.random(min, max); // Random number
gsap.utils.snap(5); // Snap to increment (returns function)
gsap.utils.clamp(min, max, value); // Clamp value
gsap.utils.interpolate(start, end, progress); // Linear interpolation
gsap.utils.wrap(min, max); // Wrap value (returns function)
gsap.utils.normalize(min, max, value); // Normalize to 0-1
gsap.utils.mapRange(inMin, inMax, outMin, outMax, value); // Map value
// Selector helpers
gsap.utils.selector(element); // Create scoped selector function
// Distribution helpers
gsap.utils.distribute({
base: 0,
amount: 100,
from: "center",
ease: "power2.out"
});Common Patterns
// Fade in on scroll
gsap.from(".element", {
opacity: 0,
y: 50,
scrollTrigger: {
trigger: ".element",
start: "top 80%",
scrub: 1
}
});
// Pin while scrolling
ScrollTrigger.create({
trigger: ".panel",
pin: true,
start: "top top",
end: "+=500"
});
// Horizontal scroll
gsap.to(".panels", {
xPercent: -100,
ease: "none",
scrollTrigger: {
trigger: ".container",
pin: true,
scrub: 1,
end: () => "+=" + document.querySelector(".panels").offsetWidth
}
});
// Parallax
gsap.to(".bg", {
y: 300,
ease: "none",
scrollTrigger: {
trigger: ".section",
start: "top bottom",
end: "bottom top",
scrub: true
}
});
// Batch stagger
ScrollTrigger.batch(".box", {
onEnter: batch => gsap.to(batch, { opacity: 1, y: 0, stagger: 0.15 }),
start: "top 80%"
});GSAP & ScrollTrigger Common Patterns
A collection of copy-paste ready patterns for common web animation scenarios.
---
Table of Contents
1. Scroll-Triggered Animations 2. Pinning Patterns 3. Horizontal Scrolling 4. Parallax Effects 5. Stagger Animations 6. Timeline Sequences 7. Text Animations 8. Image Reveals 9. Navigation & UI 10. Page Transitions 11. Scroll Progress Indicators 12. Image Sequences 13. Responsive Patterns 14. Integration Patterns
---
Scroll-Triggered Animations
1.1 Basic Fade In on Scroll
// Fade in element when it enters viewport
gsap.from(".fade-in", {
opacity: 0,
y: 50,
duration: 1,
scrollTrigger: {
trigger: ".fade-in",
start: "top 80%", // When top of element hits 80% of viewport
toggleActions: "play none none none" // Play once
}
});1.2 Fade In with Scrub (Scroll-Controlled)
// Animation synced to scroll position
gsap.from(".fade-in-scrub", {
opacity: 0,
y: 100,
scrollTrigger: {
trigger: ".fade-in-scrub",
start: "top bottom",
end: "top center",
scrub: true // Smooth scrubbing (boolean or number for lag)
}
});1.3 Batch Fade In Multiple Elements
// Efficiently animate many elements
ScrollTrigger.batch(".batch-item", {
onEnter: batch => gsap.from(batch, {
opacity: 0,
y: 60,
stagger: 0.15,
duration: 0.8,
ease: "power2.out"
}),
start: "top 90%",
once: true // Only trigger once
});1.4 Fade In from Different Directions
// Left
gsap.from(".from-left", {
x: -100,
opacity: 0,
scrollTrigger: {
trigger: ".from-left",
start: "top 80%",
toggleActions: "play none none reverse"
}
});
// Right
gsap.from(".from-right", {
x: 100,
opacity: 0,
scrollTrigger: { trigger: ".from-right", start: "top 80%" }
});
// Scale up
gsap.from(".scale-in", {
scale: 0.8,
opacity: 0,
scrollTrigger: { trigger: ".scale-in", start: "top 80%" }
});1.5 Toggle Classes on Scroll
// Add/remove CSS class based on scroll position
ScrollTrigger.create({
trigger: ".section",
start: "top center",
end: "bottom center",
toggleClass: { targets: ".section", className: "active" },
// Or specific onEnter/onLeave actions
onEnter: () => document.querySelector(".section").classList.add("active"),
onLeave: () => document.querySelector(".section").classList.remove("active")
});---
Pinning Patterns
2.1 Basic Pin (Fixed Position While Scrolling)
// Pin element while scrolling past it
ScrollTrigger.create({
trigger: ".pin-section",
start: "top top",
end: "+=500", // Pin for 500px of scrolling
pin: true,
markers: true // Debug markers (remove in production)
});2.2 Pin with Content Animation
// Pin section and animate content inside
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".pin-animation",
start: "top top",
end: "+=1000",
pin: true,
scrub: 1
}
});
tl.from(".content h1", { opacity: 0, y: 100 })
.from(".content p", { opacity: 0, y: 50 }, "-=0.5")
.to(".content img", { scale: 1.2, rotation: 5 });2.3 Pin Until Another Element
// Pin until bottom of another section
ScrollTrigger.create({
trigger: ".hero",
endTrigger: ".next-section", // Different end trigger
start: "top top",
end: "top bottom", // When top of next-section hits viewport bottom
pin: true,
pinSpacing: false // Don't add extra space
});2.4 Multiple Pinned Sections (Stacked Cards)
// Each section pins, then next one takes over
gsap.utils.toArray(".panel").forEach((panel, i) => {
ScrollTrigger.create({
trigger: panel,
start: "top top",
pin: true,
pinSpacing: false
});
});2.5 Pin with Snap Points
// Snap to specific positions while pinned
gsap.timeline({
scrollTrigger: {
trigger: ".snap-section",
start: "top top",
end: "+=2000",
pin: true,
scrub: 1,
snap: {
snapTo: [0, 0.33, 0.66, 1], // Snap to 0%, 33%, 66%, 100%
duration: { min: 0.2, max: 0.5 },
ease: "power1.inOut"
}
}
});---
Horizontal Scrolling
3.1 Basic Horizontal Scroll
// Scroll horizontally when scrolling vertically
gsap.to(".horizontal-section", {
x: () => -(document.querySelector(".horizontal-section").scrollWidth - window.innerWidth),
ease: "none",
scrollTrigger: {
trigger: ".horizontal-wrapper",
start: "top top",
end: () => "+=" + document.querySelector(".horizontal-section").scrollWidth,
scrub: 1,
pin: true,
anticipatePin: 1
}
});3.2 Horizontal Scroll with Multiple Sections
// Scroll through multiple panels
const sections = gsap.utils.toArray(".panel");
const scrollTween = gsap.to(sections, {
xPercent: -100 * (sections.length - 1),
ease: "none",
scrollTrigger: {
trigger: ".container",
pin: true,
scrub: 1,
end: () => "+=" + document.querySelector(".container").offsetWidth
}
});
// Animate each panel individually
sections.forEach((section, i) => {
gsap.from(section.querySelector(".content"), {
opacity: 0,
scale: 0.8,
scrollTrigger: {
trigger: section,
containerAnimation: scrollTween, // Link to horizontal scroll
start: "left center",
end: "right center",
scrub: true
}
});
});3.3 Horizontal Scroll with Snap
gsap.to(".slides", {
xPercent: -100 * (sections.length - 1),
ease: "none",
scrollTrigger: {
trigger: ".slider-container",
pin: true,
scrub: 1,
snap: 1 / (sections.length - 1), // Snap to each section
end: () => "+=" + document.querySelector(".slides").offsetWidth
}
});---
Parallax Effects
4.1 Simple Parallax (Different Speeds)
// Background moves slower than foreground
gsap.to(".bg", {
y: 200,
ease: "none",
scrollTrigger: {
trigger: ".parallax-section",
start: "top bottom",
end: "bottom top",
scrub: true
}
});
// Foreground moves faster
gsap.to(".fg", {
y: -100,
ease: "none",
scrollTrigger: {
trigger: ".parallax-section",
start: "top bottom",
end: "bottom top",
scrub: true
}
});4.2 Multi-Layer Parallax
// Multiple layers with different speeds
gsap.utils.toArray(".parallax-layer").forEach(layer => {
const depth = layer.dataset.depth; // data-depth="0.2" in HTML
const movement = -(layer.offsetHeight * depth);
gsap.to(layer, {
y: movement,
ease: "none",
scrollTrigger: {
trigger: ".parallax-container",
start: "top bottom",
end: "bottom top",
scrub: true
}
});
});4.3 Parallax Hero Section
// Hero image zooms out while scrolling
gsap.to(".hero-image", {
scale: 1.5,
y: 300,
ease: "none",
scrollTrigger: {
trigger: ".hero",
start: "top top",
end: "bottom top",
scrub: true
}
});
// Hero text fades out
gsap.to(".hero-text", {
opacity: 0,
y: 100,
ease: "none",
scrollTrigger: {
trigger: ".hero",
start: "top top",
end: "center top",
scrub: true
}
});---
Stagger Animations
5.1 Basic Stagger
// Stagger animation across multiple elements
gsap.from(".card", {
opacity: 0,
y: 50,
duration: 0.8,
stagger: 0.2, // 0.2s delay between each
scrollTrigger: {
trigger: ".card-container",
start: "top 80%"
}
});5.2 Stagger from Center
// Animate from center outward
gsap.from(".item", {
scale: 0,
opacity: 0,
duration: 0.6,
stagger: {
each: 0.1,
from: "center", // "start", "center", "end", "edges", "random"
ease: "power2.out"
},
scrollTrigger: { trigger: ".grid", start: "top 80%" }
});5.3 Grid Stagger
// Stagger in a grid pattern
gsap.from(".grid-item", {
opacity: 0,
scale: 0.5,
duration: 0.6,
stagger: {
grid: "auto", // Or [rows, columns] like [3, 4]
from: "center",
amount: 1.5 // Total time for all staggers
},
scrollTrigger: { trigger: ".grid", start: "top 80%" }
});5.4 Text Stagger (Words/Characters)
// Split text into words
const text = new SplitText(".title", { type: "words" });
gsap.from(text.words, {
opacity: 0,
y: 50,
rotationX: -90,
stagger: 0.05,
scrollTrigger: { trigger: ".title", start: "top 80%" }
});
// Split text into characters
const chars = new SplitText(".subtitle", { type: "chars" });
gsap.from(chars.chars, {
opacity: 0,
scale: 0,
stagger: 0.02,
ease: "back.out(1.7)",
scrollTrigger: { trigger: ".subtitle", start: "top 80%" }
});---
Timeline Sequences
6.1 Basic Timeline Sequence
// Chained animations
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".sequence-section",
start: "top 80%"
}
});
tl.from(".heading", { opacity: 0, y: -50, duration: 0.6 })
.from(".subheading", { opacity: 0, y: -30, duration: 0.5 }, "-=0.3") // Overlap by 0.3s
.from(".button", { scale: 0, duration: 0.4, ease: "back.out(1.7)" });6.2 Timeline with Labels
// Use labels for complex timing
const tl = gsap.timeline({
scrollTrigger: { trigger: ".story", start: "top center" }
});
tl.addLabel("start")
.from(".hero", { opacity: 0, scale: 0.5 }, "start")
.from(".text", { opacity: 0, x: -100 }, "start+=0.3")
.addLabel("reveal")
.from(".image", { opacity: 0, scale: 1.2 }, "reveal")
.from(".caption", { opacity: 0 }, "reveal+=0.5");6.3 Scrubbed Timeline
// Timeline controlled by scroll
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".animation-section",
start: "top top",
end: "+=1500",
scrub: 1,
pin: true
}
});
tl.to(".box1", { x: 500, rotation: 360 })
.to(".box2", { y: 300, scale: 2 }, "-=0.5")
.to(".box3", { opacity: 0, duration: 0.3 });---
Text Animations
7.1 Text Reveal (Line by Line)
// Using SplitText (Club GreenSock plugin)
const split = new SplitText(".text-reveal", { type: "lines" });
gsap.from(split.lines, {
opacity: 0,
y: 20,
stagger: 0.1,
duration: 0.6,
scrollTrigger: { trigger: ".text-reveal", start: "top 80%" }
});7.2 Scrambled Text Effect
// Using ScrambleText (Club GreenSock plugin)
gsap.to(".scramble", {
duration: 2,
scrambleText: {
text: "REVEALED TEXT",
chars: "lowerCase",
speed: 0.3
},
scrollTrigger: { trigger: ".scramble", start: "top 80%" }
});7.3 Typewriter Effect
// Simple typewriter using text content
const text = "This text appears letter by letter.";
const element = document.querySelector(".typewriter");
gsap.to(element, {
duration: text.length * 0.05,
text: { value: text },
ease: "none",
scrollTrigger: { trigger: element, start: "top 80%" }
});7.4 Text Highlight on Scroll
// Highlight text as you scroll through it
gsap.to(".highlight", {
backgroundSize: "100% 100%",
ease: "none",
scrollTrigger: {
trigger: ".highlight",
start: "top 80%",
end: "top 20%",
scrub: true
}
});
// CSS required:
// .highlight {
// background: linear-gradient(to right, yellow 0%, yellow 100%);
// background-size: 0% 100%;
// background-repeat: no-repeat;
// }---
Image Reveals
8.1 Image Curtain Reveal
<!-- HTML Structure -->
<div class="img-curtain">
<div class="curtain"></div>
<img src="image.jpg" alt="">
</div>// Curtain slides to reveal image
gsap.to(".curtain", {
xPercent: 100,
duration: 1,
ease: "power2.inOut",
scrollTrigger: {
trigger: ".img-curtain",
start: "top 80%"
}
});/* CSS */
.img-curtain {
position: relative;
overflow: hidden;
}
.curtain {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #000;
z-index: 1;
}8.2 Image Scale Reveal
// Image scales up and reveals from clip-path
gsap.from(".img-scale", {
scale: 1.3,
clipPath: "inset(100% 0% 0% 0%)",
duration: 1.5,
ease: "power2.out",
scrollTrigger: {
trigger: ".img-scale",
start: "top 80%"
}
});8.3 Image Parallax Zoom
// Image zooms while container scrolls
gsap.to(".img-zoom img", {
scale: 1.2,
ease: "none",
scrollTrigger: {
trigger: ".img-zoom",
start: "top bottom",
end: "bottom top",
scrub: true
}
});---
Navigation & UI
9.1 Sticky Header with Hide/Show
// Hide on scroll down, show on scroll up
let lastScroll = 0;
ScrollTrigger.create({
start: 100,
end: 99999,
onUpdate: (self) => {
const currentScroll = self.scroll();
if (currentScroll > lastScroll && currentScroll > 100) {
// Scrolling down
gsap.to(".header", { y: -100, duration: 0.3, ease: "power2.out" });
} else {
// Scrolling up
gsap.to(".header", { y: 0, duration: 0.3, ease: "power2.out" });
}
lastScroll = currentScroll;
}
});9.2 Active Nav Item Based on Section
// Highlight nav item when section is in view
const sections = gsap.utils.toArray("section");
sections.forEach((section, i) => {
ScrollTrigger.create({
trigger: section,
start: "top center",
end: "bottom center",
onEnter: () => setActiveNav(i),
onEnterBack: () => setActiveNav(i)
});
});
function setActiveNav(index) {
document.querySelectorAll(".nav-item").forEach((item, i) => {
item.classList.toggle("active", i === index);
});
}9.3 Smooth Scroll to Anchor
// Click anchor link to smooth scroll
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener("click", function(e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute("href"));
gsap.to(window, {
duration: 1,
scrollTo: { y: target, offsetY: 100 }, // 100px offset for header
ease: "power2.inOut"
});
});
});9.4 Drawer/Sidebar Animation
// Open drawer
function openDrawer() {
gsap.to(".drawer", {
x: 0,
duration: 0.4,
ease: "power2.out"
});
gsap.to(".overlay", {
opacity: 1,
duration: 0.3,
pointerEvents: "all"
});
}
// Close drawer
function closeDrawer() {
gsap.to(".drawer", {
x: "100%", // Or -100% for left drawer
duration: 0.4,
ease: "power2.in"
});
gsap.to(".overlay", {
opacity: 0,
duration: 0.3,
pointerEvents: "none"
});
}---
Page Transitions
10.1 Basic Page Transition (Overlay)
// On link click
function pageTransition(url) {
const tl = gsap.timeline({
onComplete: () => window.location.href = url
});
tl.to(".transition-overlay", {
yPercent: 0,
duration: 0.5,
ease: "power2.inOut"
})
.to(".transition-overlay", {
yPercent: -100,
duration: 0.5,
ease: "power2.inOut"
}, "+=0.1");
}
// Attach to links
document.querySelectorAll("a").forEach(link => {
link.addEventListener("click", (e) => {
e.preventDefault();
pageTransition(link.href);
});
});10.2 Fade Page Transition
// Fade out current page, load new page
function fadeTransition(url) {
gsap.to(".page-container", {
opacity: 0,
duration: 0.5,
onComplete: () => {
window.location.href = url;
}
});
}
// On page load, fade in
window.addEventListener("load", () => {
gsap.from(".page-container", {
opacity: 0,
duration: 0.5
});
});---
Scroll Progress Indicators
11.1 Horizontal Progress Bar
// Progress bar at top of page
gsap.to(".progress-bar", {
scaleX: 1,
ease: "none",
scrollTrigger: {
start: "top top",
end: "bottom bottom",
scrub: 0.3
}
});
// CSS: .progress-bar { transform-origin: left; }11.2 Circular Progress Indicator
// Circular progress (SVG circle)
const circle = document.querySelector(".progress-circle");
const length = circle.getTotalLength();
// Set up circle
gsap.set(circle, {
strokeDasharray: length,
strokeDashoffset: length
});
// Animate on scroll
gsap.to(circle, {
strokeDashoffset: 0,
ease: "none",
scrollTrigger: {
start: "top top",
end: "bottom bottom",
scrub: 0.3
}
});11.3 Section Progress (Multi-step)
// Update progress indicator through sections
const sections = gsap.utils.toArray(".step");
const progressDots = gsap.utils.toArray(".progress-dot");
sections.forEach((section, i) => {
ScrollTrigger.create({
trigger: section,
start: "top center",
end: "bottom center",
onEnter: () => activateDot(i),
onEnterBack: () => activateDot(i)
});
});
function activateDot(index) {
progressDots.forEach((dot, i) => {
if (i <= index) {
dot.classList.add("active");
} else {
dot.classList.remove("active");
}
});
}---
Image Sequences
12.1 Canvas Image Sequence on Scroll
// Scrub through image sequence
const canvas = document.querySelector("#canvas");
const context = canvas.getContext("2d");
const frameCount = 148;
const images = [];
const imageSeq = { frame: 0 };
// Preload images
for (let i = 0; i < frameCount; i++) {
const img = new Image();
img.src = `./frames/frame_${String(i).padStart(4, '0')}.jpg`;
images.push(img);
}
// Render frame
function render() {
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(images[imageSeq.frame], 0, 0);
}
// Animate frame property on scroll
gsap.to(imageSeq, {
frame: frameCount - 1,
snap: "frame",
ease: "none",
scrollTrigger: {
trigger: ".image-sequence",
start: "top top",
end: "+=3000",
pin: true,
scrub: 0.5,
onUpdate: render
}
});
// Initial render
images[0].onload = render;---
Responsive Patterns
13.1 Different Animations per Breakpoint
// Use ScrollTrigger.matchMedia for responsive animations
ScrollTrigger.matchMedia({
// Desktop
"(min-width: 800px)": function() {
gsap.to(".element", {
x: 500,
scrollTrigger: {
trigger: ".element",
start: "top 80%",
scrub: true
}
});
},
// Mobile
"(max-width: 799px)": function() {
gsap.to(".element", {
y: 200, // Different animation on mobile
scrollTrigger: {
trigger: ".element",
start: "top 80%",
scrub: true
}
});
}
});13.2 Kill Animations on Mobile
// Disable ScrollTrigger animations on mobile
if (window.innerWidth > 768) {
// Desktop-only animations
gsap.from(".desktop-animation", {
opacity: 0,
y: 100,
scrollTrigger: {
trigger: ".desktop-animation",
start: "top 80%"
}
});
} else {
// Simple CSS fallback on mobile
gsap.set(".desktop-animation", { opacity: 1, y: 0 });
}---
Integration Patterns
14.1 GSAP + React (useGSAP hook)
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
function Component() {
const containerRef = useRef();
useGSAP(() => {
gsap.from(".box", {
opacity: 0,
y: 50,
scrollTrigger: {
trigger: ".box",
start: "top 80%"
}
});
}, { scope: containerRef }); // Scope to container
return <div ref={containerRef}>...</div>;
}14.2 GSAP + Three.js
// Animate Three.js camera on scroll
gsap.to(camera.position, {
z: 10,
ease: "none",
scrollTrigger: {
trigger: ".threejs-section",
start: "top top",
end: "bottom top",
scrub: true
}
});
// Animate object rotation
gsap.to(mesh.rotation, {
y: Math.PI * 2,
ease: "none",
scrollTrigger: {
trigger: ".threejs-section",
scrub: 1
}
});14.3 GSAP + Locomotive Scroll
// Use Locomotive Scroll as the scroller
import LocomotiveScroll from 'locomotive-scroll';
const locoScroll = new LocomotiveScroll({
el: document.querySelector(".smooth-scroll"),
smooth: true
});
// Tell ScrollTrigger to use Locomotive Scroll
ScrollTrigger.scrollerProxy(".smooth-scroll", {
scrollTop(value) {
return arguments.length ? locoScroll.scrollTo(value, 0, 0) : locoScroll.scroll.instance.scroll.y;
},
getBoundingClientRect() {
return { top: 0, left: 0, width: window.innerWidth, height: window.innerHeight };
}
});
// Update ScrollTrigger when Locomotive Scroll updates
locoScroll.on("scroll", ScrollTrigger.update);
ScrollTrigger.addEventListener("refresh", () => locoScroll.update());---
Best Practices
Performance Optimization
// 1. Use will-change CSS for animated elements
gsap.set(".animated", { willChange: "transform, opacity" });
// 2. Kill animations when not needed
const st = ScrollTrigger.create({ /* ... */ });
st.kill(); // Clean up
// 3. Batch similar animations
ScrollTrigger.batch(".item", { /* ... */ });
// 4. Use scrub for smooth scroll-driven animations
scrub: 1 // 1 second lag for smoothness
// 5. Limit markers in production
markers: process.env.NODE_ENV === "development"Refresh After DOM Changes
// After images load, layout shifts, etc.
window.addEventListener("load", () => {
ScrollTrigger.refresh();
});
// Or after specific events
imagesLoaded(".gallery", () => {
ScrollTrigger.refresh();
});Clean Up on Unmount (React/Vue)
// React useEffect cleanup
useEffect(() => {
const st = ScrollTrigger.create({ /* ... */ });
return () => {
st.kill(); // Clean up on unmount
};
}, []);---
Common Gotchas
❌ Forgetting to Register Plugins
// WRONG - ScrollTrigger won't work
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.to(".box", { scrollTrigger: { /* ... */ } }); // Error!
// CORRECT
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
gsap.to(".box", { scrollTrigger: { /* ... */ } }); // Works!❌ Animating Before DOM Ready
// WRONG - Elements may not exist yet
gsap.from(".box", { opacity: 0 });
// CORRECT
window.addEventListener("load", () => {
gsap.from(".box", { opacity: 0 });
});
// Or with DOMContentLoaded
document.addEventListener("DOMContentLoaded", () => {
gsap.from(".box", { opacity: 0 });
});❌ Not Refreshing After Layout Changes
// WRONG - ScrollTrigger positions are stale
loadImages().then(() => {
// Images loaded, layout shifted
// ScrollTrigger positions are now wrong!
});
// CORRECT
loadImages().then(() => {
ScrollTrigger.refresh(); // Recalculate positions
});---
Quick Copy-Paste Starter
// Complete starter template
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(ScrollTrigger);
// Wait for DOM
document.addEventListener("DOMContentLoaded", () => {
// Fade in on scroll
gsap.utils.toArray(".fade-in").forEach(element => {
gsap.from(element, {
opacity: 0,
y: 50,
duration: 1,
scrollTrigger: {
trigger: element,
start: "top 80%",
toggleActions: "play none none reverse"
}
});
});
// Parallax
gsap.to(".parallax", {
y: 200,
ease: "none",
scrollTrigger: {
trigger: ".parallax-section",
start: "top bottom",
end: "bottom top",
scrub: true
}
});
// Pin section
ScrollTrigger.create({
trigger: ".pin-section",
start: "top top",
end: "+=500",
pin: true
});
// Refresh after images load
window.addEventListener("load", () => {
ScrollTrigger.refresh();
});
});---
Resources
---
Remember: Start simple, test often, and use markers: true during development to debug ScrollTrigger positions.
GSAP Easing Functions - Visual Guide
What Are Easings?
Easings control the rate of change during an animation, making motion feel more natural and expressive. They define the acceleration curve between the start and end of an animation.
- Linear: Constant speed (robotic, unnatural)
- Ease In: Starts slow, ends fast (acceleration)
- Ease Out: Starts fast, ends slow (deceleration) - Most common
- Ease InOut: Starts slow, speeds up, ends slow (smooth both ends)
Why Easings Matter
// Without proper easing - feels robotic
gsap.to(".box", { x: 500, duration: 1, ease: "none" });
// With easing - feels natural
gsap.to(".box", { x: 500, duration: 1, ease: "power2.out" });The 80/20 Rule: Most UI animations use power2.out, power3.out, or back.out.
---
Easing Families
1. Power Easings (Most Common)
Power easings use polynomial curves. Higher numbers = more aggressive curves.
power1 (Quad)
Curve Shape:
in: ╱───── Gentle acceleration
out: ─────╲ Gentle deceleration
inOut: ╱───╲ Gentle both endsUse Cases:
- Subtle UI transitions
- Small movements (tooltips, dropdowns)
- Background animations that shouldn't grab attention
Example:
// Tooltip fade in
gsap.from(".tooltip", {
opacity: 0,
y: -10,
duration: 0.3,
ease: "power1.out"
});---
power2 (Cubic) ⭐ MOST POPULAR
Curve Shape:
in: ╱╱──── Moderate acceleration
out: ────╲╲ Moderate deceleration
inOut: ╱──╲ Moderate both endsUse Cases:
- Default choice for most UI animations
- Button clicks, modal animations
- Element entrances/exits
- Smooth scrolling
Example:
// Modal slide in
gsap.from(".modal", {
y: 50,
opacity: 0,
duration: 0.5,
ease: "power2.out" // ⭐ Most common choice
});---
power3 (Quart)
Curve Shape:
in: ╱╱╱─── Strong acceleration
out: ───╲╲╲ Strong deceleration
inOut: ╱─╲ Strong both endsUse Cases:
- Hero animations
- Page transitions
- Large movements across screen
- Attention-grabbing effects
Example:
// Hero section reveal
gsap.from(".hero", {
scale: 0.8,
opacity: 0,
duration: 1,
ease: "power3.out"
});---
power4 (Quint)
Curve Shape:
in: ╱╱╱╱── Very strong acceleration
out: ──╲╲╲╲ Very strong deceleration
inOut: ╱╲ Very strong both endsUse Cases:
- Dramatic reveals
- Full-screen transitions
- Heavy objects (metaphorically)
- When you want maximum impact
Example:
// Full-screen overlay
gsap.to(".overlay", {
scale: 1,
opacity: 1,
duration: 0.8,
ease: "power4.out"
});---
2. Back Easings (Overshoot)
Back easings overshoot the target, then settle back. Creates anticipation and playfulness.
Curve Shape:
in: ╲╱──── Backs up, then accelerates
out: ────╱╲ Overshoots, then settles
inOut: ╲╱─╱╲ Both ends overshootParameters: back.out(1.7) - Higher number = more overshoot (default: 1.7)
Use Cases:
- Playful UI elements
- Buttons with personality
- Attention-grabbing reveals
- Fun micro-interactions
Example:
// Button with bounce
gsap.from(".button", {
scale: 0,
duration: 0.5,
ease: "back.out(1.7)" // Classic overshoot
});
// Extreme overshoot for fun effect
gsap.from(".badge", {
scale: 0,
rotation: -180,
duration: 0.8,
ease: "back.out(3)" // More dramatic
});Pro Tip: Use back.out(1.2) for subtle overshoot, back.out(2.5) for exaggerated effect.
---
3. Elastic Easings (Spring/Rubber Band)
Elastic easings create a spring/bounce effect, oscillating around the target.
Curve Shape:
in: ╲╱╲╱── Oscillates, then accelerates
out: ──╱╲╱╲ Overshoots multiple times, then settles
inOut: ╲╱╲╱╲ Both ends oscillateParameters: elastic.out(amplitude, period)
- amplitude: Strength of oscillation (default: 1)
- period: Frequency of oscillation (default: 0.3)
Use Cases:
- Cartoonish effects
- Game UI elements
- Fun landing pages
- Notification badges
- Use sparingly - can feel gimmicky
Example:
// Notification badge
gsap.from(".badge", {
scale: 0,
duration: 1,
ease: "elastic.out(1, 0.3)"
});
// Stronger spring effect
gsap.from(".icon", {
y: -50,
duration: 1.5,
ease: "elastic.out(1.5, 0.4)" // More bounce
});Warning: Elastic can feel unprofessional if overused. Best for gaming/playful interfaces.
---
4. Bounce Easings
Bounce easings simulate a bouncing ball landing.
Curve Shape:
in: ╲╱╲╱╲── Bounces, then accelerates
out: ──╲╱╲╱╲ Lands with decreasing bounces
inOut: ╲╱─╱╲ Both ends bounceUse Cases:
- Elements "dropping" into place
- Playful UI (games, children's apps)
- Landing animations
- Fun effects (use sparingly)
Example:
// Element drops in
gsap.from(".card", {
y: -200,
duration: 1,
ease: "bounce.out"
});
// Multiple cards with stagger
gsap.from(".card", {
y: -100,
opacity: 0,
duration: 1,
ease: "bounce.out",
stagger: 0.1
});Pro Tip: Combine with power2.in for objects falling before bouncing.
---
5. Circ Easings (Circular)
Circular easings use a quarter-circle curve. Creates sharp acceleration/deceleration.
Curve Shape:
in: ╱╱──── Sharp acceleration (quarter circle)
out: ────╲╲ Sharp deceleration
inOut: ╱──╲ Sharp both endsUse Cases:
- Fast, snappy movements
- Material Design-style animations
- Quick transitions
- When you want instant impact
Example:
// Quick slide-in drawer
gsap.to(".drawer", {
x: 0,
duration: 0.3,
ease: "circ.out"
});---
6. Expo Easings (Exponential)
Exponential easings create very dramatic acceleration/deceleration curves.
Curve Shape:
in: ╱╱╱─── Extremely slow start, then explodes
out: ───╲╲╲ Extremely fast start, then crawls
inOut: ╱─╲ Extreme both endsUse Cases:
- Dramatic reveals
- Cinematic effects
- Zoom-in/zoom-out animations
- When you want maximum drama
Example:
// Dramatic zoom in
gsap.from(".hero-image", {
scale: 5,
opacity: 0,
duration: 1.5,
ease: "expo.out"
});
// Intense page transition
gsap.to(".page", {
x: -window.innerWidth,
duration: 1,
ease: "expo.inOut"
});---
7. Sine Easings
Sine easings are extremely gentle, using a sine wave curve.
Curve Shape:
in: ╱───── Very gentle acceleration
out: ─────╲ Very gentle deceleration
inOut: ╱───╲ Very gentle both endsUse Cases:
- Subtle, barely-noticeable animations
- Background movements
- Ambient effects
- Parallax scrolling
- When animation should be invisible
Example:
// Gentle parallax
gsap.to(".bg", {
y: 100,
ease: "sine.out",
scrollTrigger: {
scrub: true
}
});
// Subtle float animation
gsap.to(".cloud", {
y: "+=20",
duration: 3,
ease: "sine.inOut",
yoyo: true,
repeat: -1
});---
8. None (Linear)
No easing at all - constant speed throughout.
Curve Shape: ────── Perfectly straight lineUse Cases:
- ScrollTrigger with scrub (let user control speed)
- Loading bars (constant progress)
- Background loops
- Mechanical movements
- Rarely used for regular animations
Example:
// Horizontal scroll (user controls speed)
gsap.to(".panels", {
xPercent: -100,
ease: "none", // User controls via scroll
scrollTrigger: {
scrub: true
}
});
// Loading bar
gsap.to(".progress", {
width: "100%",
duration: 5,
ease: "none"
});---
Quick Decision Tree
1. What's the purpose?
Subtle UI transition → power1.out or power2.out
Standard animation → power2.out ⭐ (default choice)
Hero/dramatic effect → power3.out or power4.out
Playful/fun → back.out(1.7) or elastic.out()
Game/cartoon → bounce.out or elastic.out()
Scroll-driven → none (with scrub)
Background/ambient → sine.inOut
---
2. How noticeable should it be?
Subtle ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→ Dramatic
sine.out power1.out power2.out power3.out expo.out
⭐ Start here
Gentle ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━→ Playful
circ.out back.out(1.2) back.out(1.7) elastic.out()---
Common Patterns by Use Case
UI Animations (80% of use cases)
// Standard choice
ease: "power2.out"
// Slightly more dramatic
ease: "power3.out"
// Playful
ease: "back.out(1.7)"Scroll Animations
// Let user control speed
ease: "none",
scrollTrigger: { scrub: true }
// Smooth snap scrolling
ease: "power2.inOut",
scrollTrigger: { snap: 1 }Micro-interactions
// Button hover
ease: "power1.out", duration: 0.2
// Toggle switch
ease: "back.out(2)", duration: 0.4
// Ripple effect
ease: "power2.out", duration: 0.6Page Transitions
// Standard page transition
ease: "power3.inOut", duration: 0.8
// Dramatic reveal
ease: "expo.out", duration: 1.2Loading States
// Loading spinner
ease: "none", repeat: -1
// Progress bar
ease: "power2.out", duration: 2---
Easing Combinations
Sequential Easings (Timeline)
const tl = gsap.timeline();
// Slide in fast, settle slow
tl.to(".card", { y: 0, duration: 0.3, ease: "power3.in" })
.to(".card", { scale: 1, duration: 0.4, ease: "back.out(2)" });Staggered with Different Easings
gsap.from(".item", {
opacity: 0,
y: 50,
duration: 0.6,
ease: "power2.out",
stagger: {
each: 0.1,
ease: "power1.inOut" // Easing for stagger timing itself
}
});Physics-based Fallback
// Light object (fast)
gsap.to(".feather", { y: 500, duration: 2, ease: "sine.in" });
// Heavy object (slow start)
gsap.to(".anvil", { y: 500, duration: 1.5, ease: "power4.in" });---
Custom Easings
Custom Bezier Curve
// Use CustomEase plugin (Club GreenSock)
gsap.registerPlugin(CustomEase);
CustomEase.create("customBounce", "0.5,0,0.75,1.5,1");
gsap.to(".box", {
x: 500,
duration: 1,
ease: "customBounce"
});Steps (Stepped Animation)
// Simulate frame-by-frame animation
gsap.to(".sprite", {
x: 500,
duration: 1,
ease: "steps(12)" // 12 discrete steps
});---
Testing Easings
Easing Visualizer (Inline Tool)
Save this as easings.html to test easings visually:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<style>
body { font-family: system-ui; padding: 40px; background: #1a1a2e; color: white; }
.box { width: 50px; height: 50px; background: #00ffcc; margin: 20px 0; border-radius: 8px; }
button { padding: 10px 20px; margin: 5px; background: #00ffcc; border: none; border-radius: 5px; cursor: pointer; }
</style>
</head>
<body>
<h1>GSAP Easing Tester</h1>
<div id="boxes"></div>
<button onclick="testAll()">Test All Easings</button>
<script>
const easings = [
"none",
"power1.out", "power2.out", "power3.out", "power4.out",
"back.out(1.7)", "elastic.out(1, 0.3)", "bounce.out",
"circ.out", "expo.out", "sine.out"
];
const container = document.getElementById('boxes');
easings.forEach(ease => {
const label = document.createElement('div');
label.textContent = ease;
label.style.marginTop = '20px';
container.appendChild(label);
const box = document.createElement('div');
box.className = 'box';
box.dataset.ease = ease;
container.appendChild(box);
});
function testAll() {
document.querySelectorAll('.box').forEach(box => {
gsap.fromTo(box,
{ x: 0 },
{ x: 500, duration: 1.5, ease: box.dataset.ease }
);
});
}
</script>
</body>
</html>Online Tools:
---
Performance Notes
Do Easings Affect Performance?
No - Easings are mathematical functions with negligible overhead. Choose based on aesthetics, not performance.
Exception: Custom bezier curves with many control points can be slightly slower (still negligible).
---
Common Mistakes
❌ Using Linear for UI Animations
// Feels robotic
gsap.to(".button", { scale: 1.1, duration: 0.3, ease: "none" });✅ Fix: Use power2.out
gsap.to(".button", { scale: 1.1, duration: 0.3, ease: "power2.out" });---
❌ Over-using Elastic/Bounce
// Every element bounces - feels gimmicky
gsap.from(".item", { scale: 0, ease: "elastic.out(2, 0.3)", stagger: 0.1 });✅ Fix: Use sparingly for key elements
// Only hero element has elastic
gsap.from(".hero", { scale: 0, ease: "elastic.out(1, 0.3)" });
gsap.from(".item", { scale: 0, ease: "back.out(1.7)", stagger: 0.1 });---
❌ Wrong Direction (in vs out)
// Element exits with ease.in (feels slow to start)
gsap.to(".modal", { opacity: 0, ease: "power2.in" });✅ Fix: Use out for exits
gsap.to(".modal", { opacity: 0, ease: "power2.out" });Rule:
- Entrances →
ease.out(fast start, slow end) - Exits →
ease.out(consistent) - Both →
ease.inOut(smooth both ends)
---
Best Practices
1. Start with power2.out
It's the goldilocks easing - not too gentle, not too aggressive.
2. Match Easing to Content
- Lightweight (modals, tooltips) →
power1.outorpower2.out - Medium (cards, sections) →
power2.outorpower3.out - Heavy (full pages, heroes) →
power3.outorpower4.out
3. Be Consistent
Use the same easing family throughout your project for cohesion.
4. Test at Different Speeds
Easings behave differently at different durations:
// Too short - easing barely noticeable
duration: 0.1, ease: "power3.out"
// Sweet spot
duration: 0.4, ease: "power3.out"
// Too long - feels sluggish
duration: 2, ease: "power3.out"5. Consider Context
- Professional/Corporate →
power2.out,power3.out - Playful/Fun →
back.out,elastic.out - Luxury/Premium →
expo.out,power4.out - Fast/Responsive →
circ.out,power2.out
---
Cheat Sheet
| Use Case | Easing | Duration | Notes |
|---|---|---|---|
| Default UI | power2.out | 0.3-0.5s | Start here |
| Hero animation | power3.out | 0.8-1.2s | More impact |
| Button hover | power1.out | 0.2s | Quick & subtle |
| Modal open | power2.out | 0.4s | Smooth entrance |
| Tooltip | power1.out | 0.2s | Fast & light |
| Scroll scrub | none | N/A | User-controlled |
| Page transition | power3.inOut | 0.8s | Smooth both ends |
| Playful button | back.out(1.7) | 0.5s | Overshoot |
| Notification | back.out(2) | 0.6s | Attention-grabbing |
| Parallax | sine.out | N/A | Barely noticeable |
| Loading bar | none | Variable | Constant speed |
| Bounce-in | bounce.out | 1s | Use sparingly |
---
Summary
The 3 Easings You Need to Know:
1. power2.out ⭐ - Your default choice (80% of animations) 2. power3.out - For dramatic effects 3. back.out(1.7) - For playful interactions
Golden Rules:
- When in doubt, use
power2.out - Entrances and exits both use
.out - ScrollTrigger with scrub uses
none - Be consistent across your project
- Test different durations to find the sweet spot
Remember: Good easing makes animations feel natural and purposeful. Bad easing makes them feel robotic or gimmicky.
Related skills
How it compares
Pick gsap-scrolltrigger over generic animation skills when the stack is GSAP and scroll-linked pin/scrub/timeline behavior is required.
FAQ
What does gsap-scrolltrigger do?
Create GSAP and ScrollTrigger scroll-driven animations, timelines, and tweens.
When should I use gsap-scrolltrigger?
User builds web animations, scroll-triggered effects, or GSAP timelines.
Is gsap-scrolltrigger safe to install?
Review the Security Audits panel on this page before installing in production.