Gsap Web
This skill should be used when the user asks to "build a scroll animation", "add ScrollTrigger", "pin a section while scrolling", "scrub an animation to scroll", "create a hero timeline", "do a horizontal scroll section", "split text and animate words", "animate a layout change with GSAP Flip", "sync ScrollTrigger with Lenis", "Lenis smooth scroll jittery", "ScrollTrigger markers drift", "ScrollTrigger pins break with Lenis/Locomotive", or "set up smooth scroll with GSAP pinning". Covers GSAP timelines, ScrollTrigger, SplitText, Flip, and Lenis/Locomotive smooth-scroll sync for code-driven web motion.
- Skill ID
- iart-ai/web-animation-skills/gsap-web
- Publisher
- iart-ai
- Repository
- web-animation-skills
- Installs
- 0
- Files
- 6
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
iart-ai/web-animation-skills/gsap-webInstalls these files- README.md
- SKILL.md
- examples/hero-timeline.js
- examples/standalone-template.html
- references/scrolltrigger-cookbook.md
- references/scrolltrigger-lenis.md
What this skill tells the agent
GSAP for the Web
GSAP (GreenSock Animation Platform) is the workhorse for code-driven web motion: sequenced timelines, scroll-driven storytelling, text reveals, and layout transitions. As of GSAP 3.12+, every plugin (ScrollTrigger, SplitText, Flip, MotionPath, MorphSVG, Draggable, Observer) is 100% free, including for commercial use.
When to use
- Scroll-driven storytelling: pinned sections, parallax, progress scrubbing, horizontal scroll
- Sequenced hero animations and complex multi-element timelines with precise overlap control
- Text reveals split into chars/words/lines (SplitText)
- Layout transitions where an element changes position/size/parent (Flip)
- Any motion needing fine timing control, easing precision, or imperative orchestration
Install and register
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import { Flip } from "gsap/Flip";
gsap.registerPlugin(ScrollTrigger, SplitText, Flip);Plugins MUST be registered before use or they silently no-op. With a bundler, register once at app entry.
Core techniques
Timelines first
Prefer one timeline over many independent tweens — it gives a single playhead, relative positioning, and easy reversal.
const tl = gsap.timeline({ defaults: { ease: "power3.out", duration: 0.6 } });
tl.from(".title", { yPercent: 100, opacity: 0 })
.from(".sub", { y: 20, opacity: 0 }, "-=0.3") // start 0.3s before prev ends
.from(".cta", { scale: 0.9, opacity: 0 }, "<"); // align to prev tween STARTPosition parameter cheat sheet:
"+=0.5"/"-=0.3"— relative to the end of the timeline (gap / overlap)"<"— start of the previous tween;">"— end of the previous tween"<0.2"— 0.2s after the previous tween's start"myLabel"— at a named label added viatl.addLabel("myLabel")
Use .from() for entrances (animates FROM the given values TO current CSS), .to() for exits, .fromTo() when both ends must be explicit (most robust against re-runs).
gsap.to vs set vs quickTo
gsap.set(el, { autoAlpha: 0 }); // instant, no tween (autoAlpha = opacity + visibility)
const xTo = gsap.quickTo(el, "x", { duration: 0.4, ease: "power3" });
window.addEventListener("pointermove", (e) => xTo(e.clientX)); // fast repeated updatesautoAlpha is preferred over raw opacity because it also toggles visibility:hidden at 0, removing the element from hit-testing.
ScrollTrigger basics
gsap.to(".panel", {
xPercent: -100,
ease: "none",
scrollTrigger: {
trigger: ".wrap",
start: "top top", // when trigger top hits viewport top
end: "+=2000", // 2000px of scroll distance
pin: true, // freeze .wrap while the tween plays
scrub: 1, // tie progress to scrollbar (1 = 1s catch-up smoothing)
markers: true, // dev-only visual markers — remove for prod
},
});Key semantics:
start/endtake"triggerPos viewportPos"(e.g."top center") or"+=px".scrub: truelocks animation progress to scroll exactly;scrub: <number>adds smoothing lag.toggleActions: "play pause resume reverse"controls onEnter/onLeave/onEnterBack/onLeaveBack for non-scrubbed triggers.- Use
ease: "none"for scrubbed tweens so motion tracks scroll linearly.
Smooth scroll (Lenis) + ScrollTrigger
Pairing Lenis (or Locomotive) smooth scrolling with ScrollTrigger desyncs unless both run on one loop. Lenis smooths scroll on its own rAF while ScrollTrigger reads scroll on GSAP's ticker; when they tick independently ScrollTrigger samples a stale position. The fix: drive Lenis from GSAP's ticker and update ScrollTrigger on every Lenis scroll.
import Lenis from "lenis";
const lenis = new Lenis({ duration: 1.2, smoothWheel: true });
lenis.on("scroll", ScrollTrigger.update); // 1. update ScrollTrigger on every Lenis scroll
gsap.ticker.add((t) => lenis.raf(t * 1000)); // 2. one loop: ticker drives Lenis (seconds → ms)
gsap.ticker.lagSmoothing(0); // 3. stop GSAP catch-up jitter on heavy framesCritical: gsap.ticker passes time in seconds, lenis.raf() wants milliseconds — multiply by 1000. Do NOT also run a standalone requestAnimationFrame(raf) loop for Lenis; that double-drives it.
Common bugs:
- Jitter/stutter — a leftover
requestAnimationFrame(raf)loop competing with the ticker, or missinglagSmoothing(0). Remove the rogue loop; set lag smoothing to 0. - Markers drift from their triggers —
ScrollTrigger.updateis not subscribed tolenis.on("scroll", …). - Pins break — under Lenis do NOT set a
scrollerProxy(it scrolls the document). Under Locomotive you MUST wirescrollerProxy+pinTypeand setscroller:on every trigger. - Scroll stuck / flipped direction — missing the
* 1000unit conversion, or two loops out of order.
Cleanup on SPA unmount: gsap.ticker.remove(update) + lenis.destroy(), else loops stack per navigation. Gate behind prefers-reduced-motion (skip Lenis, run native scroll). Full Lenis + Locomotive wiring, anchor-link routing, data-lenis-prevent, React (useGSAP/useEffect), and a symptom→cause table are in references/scrolltrigger-lenis.md.
SplitText (text reveal)
const split = SplitText.create(".headline", { type: "lines, words", linesClass: "line" });
gsap.from(split.lines, { yPercent: 100, opacity: 0, stagger: 0.08, duration: 0.7, ease: "power4.out" });Gotchas:
- Wrap line-mask reveals: set
overflow: hiddenon the line wrapper soyPercent: 100hides cleanly. AddautoSplit: true(GSAP 3.13+) to re-split on font load / resize. - Call
split.revert()before re-splitting or on unmount to restore original DOM and avoid duplicated nodes. - Always split AFTER web fonts load (
document.fonts.ready.then(...)) to prevent wrong line breaks.
Flip (layout transitions)
Flip records state, lets the DOM change instantly, then animates the visual difference (FLIP technique). Ideal for grid<->list, expanding cards, and reparenting.
const state = Flip.getState(".item"); // 1. capture BEFORE
container.classList.toggle("grid"); // 2. mutate DOM/CSS (instant)
Flip.from(state, { // 3. animate the delta
duration: 0.6, ease: "power2.inOut", stagger: 0.05,
absolute: true, // take items out of flow during move (prevents reflow jitter)
});Easing quick guide
power2/3.out— entrances (fast then settle)power2.inOut— moves between two on-screen statesback.out(1.7)— overshoot/pop (number = overshoot amount)elastic.out(1, 0.3)— bouncy, use sparinglynone— scrubbed scroll tweenssteps(n)— sprite/stepped motion- Custom cubic-bezier equivalent:
CustomEase.create("x", "M0,0 C0.2,0 0,1 1,1")(CustomEase is free)
Performance and cleanup
- Animate
transform(x/y/xPercent/scale/rotation) andopacityonly — they are GPU-composited and skip layout/paint. Avoid animatingtop/left/width/height. - Set
will-change: transformon heavy/pinned elements; remove after. - Batch many similar scroll reveals with
ScrollTrigger.batch()instead of one trigger per element. - After a viewport/content change, call
ScrollTrigger.refresh().
SPA / framework cleanup is mandatory — orphaned triggers cause memory leaks and ghost pinning. Use gsap.context() (or useGSAP from @gsap/react):
useEffect(() => {
const ctx = gsap.context(() => {
// all gsap + ScrollTrigger code here
}, rootRef);
return () => ctx.revert(); // kills tweens, triggers, and reverts inline styles
}, []);Deliver & verify (standalone HTML)
Packaged helper (scripts/):scripts/seek-shot.sh anim.html 0 1.5 3freezes the?t=Nharness and screenshots each moment;scripts/contact-sheet.sh sheet.png frame-*.pngtiles them for one-glance review. Seescripts/README.md.
More from iart-ai
Use this skill in a project
Open any RiverX project, search the Skills panel for iart-ai/web-animation-skills/gsap-web, and install it in one click.
