GSAP Animations skill for Codex
Scroll storytelling without leaks or jank. Codex writes GSAP the way production sites need it: one plugin registration module, every tween and ScrollTrigger scoped with useGSAP or gsap.context so route changes and Strict Mode do not leak, timelines built from small labeled pieces, and gsap.matchMedia handling breakpoints and reduced motion. It knows why pins misbehave and when ScrollTrigger needs a refresh.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- gsap-animations Installs to ~/.agents/skills/gsap-animations/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install gsap-animations The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project gsap-animations Installs to .agents/skills/gsap-animations/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/gsap-animations.zipunzip -o gsap-animations.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) gsap-animations PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $gsap-animations, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds GSAP 3 animation in real apps - timelines with labels and the position parameter, ScrollTrigger pinning and scrubbing, SplitText, gsap.matchMedia for breakpoints and reduced motion, and leak-free cleanup with useGSAP in React or gsap.context in Vue, Svelte and Angular. Use when a user wants scroll storytelling, a pinned section, a scrubbed hero, text reveal, a complex sequenced intro, or when GSAP animations break after route changes, double-fire in React strict mode, or misplace triggers after images load.
Use it when
- A page needs pinned, scrubbed or sequenced scroll animation
- GSAP code leaks triggers or double-fires in a component framework
- Several elements must animate in a precisely timed sequence
- Text needs line, word or character reveals
Use something else when
- Simple reveal-on-scroll or hover effects CSS can do: use css-animations
- State-driven React enter, exit and layout animation: use motion-animations
How to check the result
Navigate between routes repeatedly while watching ScrollTrigger.getAll().length stay flat, resize across breakpoints, and emulate reduced motion to confirm content is visible without pinning.
The mistake it prevents
Creating tweens with unscoped selectors outside useGSAP or gsap.context, so they target other components and are never reverted on unmount.
Done well means: every tween and ScrollTrigger is created inside a scoped context and reverted on unmount, trigger positions are correct after fonts and images load, the page works and reads fine with motion reduced, and timelines are readable enough that the next person can retime them.
Before writing anything
- Read
AGENTS.mdandpackage.json. Check forgsap,@gsap/react, a smooth-scroll library (Lenis, Locomotive) and the framework/router. - GSAP (3.13+) is free for all uses, including former Club plugins such as SplitText,
MorphSVG, DrawSVG and ScrollSmoother; they ship inside the
gsappackage. Install withnpm install gsap @gsap/react(needs network; ask for approval in a sandbox). - Grep for existing
registerPlugincalls and keep one central registration module.
Setup
// lib/gsap.ts — import this everywhere instead of "gsap" directly
"use client";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(ScrollTrigger, SplitText, useGSAP);
gsap.defaults({ ease: "power2.out", duration: 0.6 });
export { gsap, ScrollTrigger, SplitText, useGSAP };
GSAP touches the DOM; in Next.js App Router or other SSR it only runs in client components.
React: useGSAP
"use client";
import { useRef } from "react";
import { gsap, useGSAP } from "@/lib/gsap";
export function Hero() {
const root = useRef<HTMLElement>(null);
useGSAP(() => {
const tl = gsap.timeline();
tl.from(".hero-title", { yPercent: 100, autoAlpha: 0 })
.from(".hero-sub", { y: 20, autoAlpha: 0 }, "<0.15")
.from(".hero-cta", { scale: 0.9, autoAlpha: 0 }, "-=0.2");
}, { scope: root });
return <section ref={root}>...</section>;
}
scopelimits selector text to descendants ofroot, so.hero-titlein another component is never touched.- Everything created inside the callback is reverted on unmount, which also fixes double animation under React Strict Mode.
- Pass
dependencies: [value](andrevertOnUpdate: trueif the animation should restart). - Animations started from event handlers must be wrapped:
const onClick = contextSafe(() => gsap.to(...)), usingconst { contextSafe } = useGSAP({ scope: root }). Otherwise they leak.
Other frameworks
// Vue 3
let ctx: gsap.Context;
onMounted(() => { ctx = gsap.context(() => { /* tweens, ScrollTriggers */ }, rootEl.value); });
onUnmounted(() => ctx.revert());
Svelte: create the context in onMount and return () => ctx.revert(). Angular: create in
ngAfterViewInit inside NgZone.runOutsideAngular, revert in ngOnDestroy.
Timelines
| Position parameter | Meaning |
|---|---|
"<" | Start with the previous tween |
"<0.2" | 0.2 s after the previous tween starts |
"-=0.3" | Overlap the end of the timeline by 0.3 s |
"+=0.5" | Gap of 0.5 s after the end |
"reveal" / "reveal+=0.2" | At (or offset from) a label added with tl.addLabel("reveal") |
- Build one master timeline from small functions that each return a timeline; nest with
master.add(intro()).add(cards(), "-=0.2"). Retiming is then one edit. - Use
autoAlphainstead ofopacityso hidden elements also getvisibility: hidden(not focusable, not clickable). - Prevent a flash of unanimated content by hiding start states in CSS
(
.hero-title { visibility: hidden }) and animatingautoAlphafrom 0, or by usinggsap.setbefore first paint;from()alone shows the end state for one frame. - Use
fromTowhen an animation may be re-run from an unknown state.
ScrollTrigger
useGSAP(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: ".story",
start: "top top",
end: "+=200%",
pin: true,
scrub: 0.5,
anticipatePin: 1,
invalidateOnRefresh: true,
// markers: true, // dev only
},
});
tl.to(".story-panel", { xPercent: -100 * 3, ease: "none" });
}, { scope: root });
| Need | Settings |
|---|---|
| Play once on enter | start: "top 80%", toggleActions: "play none none none" or once: true |
| Tie progress to scroll | scrub: true (or a number for smoothing seconds), ease: "none" on tweens |
| Hold a section while content changes | pin: true, end: "+=N%" |
| Batch reveal for many cards | ScrollTrigger.batch(".card", { onEnter: (els) => gsap.from(els, {...}) }) |
| Values depend on viewport size | Use function values (x: () => -track.scrollWidth + innerWidth) plus invalidateOnRefresh: true |
start/endstrings are “trigger-position viewport-position”:"top 80%"means the top of the trigger meets 80% down the viewport.- Create ScrollTriggers top to bottom in page order, or set
refreshPriority; pins computed out of order offset everything after them. - Call
ScrollTrigger.refresh()after late layout changes (images without dimensions, fonts, accordions, data loaded after mount). Better: give imageswidth/heightso layout is stable. - On client-side route changes, the old page’s triggers must be reverted (handled by the context) before the new page measures.
- Smooth scrolling with Lenis: drive Lenis from
gsap.ticker, callScrollTrigger.updateon Lenisscroll, and setgsap.ticker.lagSmoothing(0).
Breakpoints and reduced motion
useGSAP(() => {
const mm = gsap.matchMedia();
mm.add(
{ desktop: "(min-width: 1024px)", reduce: "(prefers-reduced-motion: reduce)" },
(ctx) => {
const { desktop, reduce } = ctx.conditions!;
if (reduce) { gsap.set(".reveal", { autoAlpha: 1 }); return; }
gsap.from(".reveal", { y: desktop ? 60 : 20, autoAlpha: 0, stagger: 0.08 });
},
);
}, { scope: root });
matchMedia reverts and re-runs its callback when a query flips, so pinning on desktop only is safe. Under reduced motion, keep content in its final state and drop pinning and scrubbing.
SplitText
SplitText.create(".headline", {
type: "lines,words",
mask: "lines",
autoSplit: true,
onSplit: (self) => gsap.from(self.words, { yPercent: 100, stagger: 0.04 }),
});
- Wait for fonts (
document.fonts.ready) or useautoSplitwithonSplitso line breaks are recomputed after font load and resize; returning the tween lets it be cleaned up. - SplitText adds
aria-labelto the parent and hides split children from assistive tech by default; keep that behavior so screen readers do not read letters one by one.
Gotchas
- Selector strings outside a scoped context match the whole document; in component code
always pass
scope. pin: truewraps the element in a pin-spacer; CSS like:first-childor flex gap rules on the parent may change. Pin a wrapper, not a grid item.- Do not pin an element whose ancestor has
transformorwill-change: transform; pinning usesposition: fixedand breaks inside a containing block (setpinReparentonly as a last resort). - Animating
x/y(transforms) is cheap; animatingtop/left/widthis layout work. markers: trueleft in production ships debug UI.- Each
gsap.toon the same property of the same element fights the previous one; useoverwrite: "auto"for hover-style re-triggers.
Verification
- Navigate away and back several times;
ScrollTrigger.getAll().lengthin the console stays constant (no leaks). - Run in React Strict Mode dev; each animation plays once.
- Resize from mobile to desktop and back; pins and positions recalculate correctly.
- Emulate reduced motion; content is visible, no pinning or scrubbing.
- Hard reload with a throttled network (Slow 4G); triggers still fire at the right spot after images and fonts land.
- Remove all
markers: true.
Output
Summarize: timelines and triggers added (with start/end values), cleanup mechanism per
component, breakpoint and reduced-motion behavior, and any ScrollTrigger.refresh() hooks.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "GSAP Animations"
short_description: "Scroll storytelling without leaks or jank"
default_prompt: "$gsap-animations Pin this section and scroll the panels horizontally with GSAP ScrollTrigger" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.