Loading...
Loading...
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.
npx skill4agent add iart-ai/web-animation-skills gsap-webimport gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import { Flip } from "gsap/Flip";
gsap.registerPlugin(ScrollTrigger, SplitText, Flip);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 START"+=0.5""-=0.3""<"">""<0.2""myLabel"tl.addLabel("myLabel").from().to().fromTo()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 updatesautoAlphaopacityvisibility:hiddengsap.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
},
});startend"triggerPos viewportPos""top center""+=px"scrub: truescrub: <number>toggleActions: "play pause resume reverse"ease: "none"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 framesgsap.tickerlenis.raf()requestAnimationFrame(raf)requestAnimationFrame(raf)lagSmoothing(0)ScrollTrigger.updatelenis.on("scroll", …)scrollerProxyscrollerProxypinTypescroller:* 1000gsap.ticker.remove(update)lenis.destroy()prefers-reduced-motiondata-lenis-preventuseGSAPuseEffectreferences/scrolltrigger-lenis.mdconst 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" });overflow: hiddenyPercent: 100autoSplit: truesplit.revert()document.fonts.ready.then(...)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)
});power2/3.outpower2.inOutback.out(1.7)elastic.out(1, 0.3)nonesteps(n)CustomEase.create("x", "M0,0 C0.2,0 0,1 1,1")transformx/y/xPercent/scale/rotationopacitytop/left/width/heightwill-change: transformScrollTrigger.batch()ScrollTrigger.refresh()gsap.context()useGSAP@gsap/reactuseEffect(() => {
const ctx = gsap.context(() => {
// all gsap + ScrollTrigger code here
}, rootRef);
return () => ctx.revert(); // kills tweens, triggers, and reverts inline styles
}, []);Packaged helper ():scripts/freezes thescripts/seek-shot.sh anim.html 0 1.5 3harness and screenshots each moment;?t=Ntiles them for one-glance review. Seescripts/contact-sheet.sh sheet.png frame-*.png.scripts/README.md
.html<script>const tl = gsap.timeline()?t=NN<script>
// ... build your master timeline as `tl` ...
const t = new URLSearchParams(location.search).get("t");
if (t !== null) { tl.pause(); tl.seek(parseFloat(t)); } // frozen at t seconds
// no ?t → plays normally
window.__ready = true; // ready signal for headless wait
console.log("duration", tl.duration());
</script>…/anim.html?t=0?t=<dur/2>?t=<dur>tl.duration()npx playwright screenshot --wait-for-timeout=500 "file://$PWD/anim.html?t=1.2" frame-mid.png?t=Nprefers-reduced-motionlinearexamples/standalone-template.html| Goal | API |
|---|---|
| Sequence with overlap | |
| Pin while scrolling | |
| Scrub to scroll | |
| Reveal lines of text | |
| Animate layout change | |
| Fast pointer follow | |
| Kill everything | |
references/scrolltrigger-cookbook.mdScrollTrigger.batch()references/scrolltrigger-lenis.mdscrollerProxyuseGSAPuseEffectexamples/hero-timeline.jsexamples/standalone-template.html?t=N