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.

  • Frontend
  • $gsap-animations
  • 201 lines
  • 2 files
  • ~2,000 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- gsap-animations

Installs to ~/.agents/skills/gsap-animations/ for every project on this machine.

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

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.

~/.agents/skills/gsap-animations/SKILL.md

--- name: gsap-animations description: Builds GSAP 3 animation in real apps - timelines with labels and the position parameter, ScrollTrigger pinning and scrubbing, SplitText,… license: MIT ---

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

  1. Read AGENTS.md and package.json. Check for gsap, @gsap/react, a smooth-scroll library (Lenis, Locomotive) and the framework/router.
  2. GSAP (3.13+) is free for all uses, including former Club plugins such as SplitText, MorphSVG, DrawSVG and ScrollSmoother; they ship inside the gsap package. Install with npm install gsap @gsap/react (needs network; ask for approval in a sandbox).
  3. Grep for existing registerPlugin calls 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>;
}
  • scope limits selector text to descendants of root, so .hero-title in 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] (and revertOnUpdate: true if the animation should restart).
  • Animations started from event handlers must be wrapped: const onClick = contextSafe(() => gsap.to(...)), using const { 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 parameterMeaning
"<"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 autoAlpha instead of opacity so hidden elements also get visibility: hidden (not focusable, not clickable).
  • Prevent a flash of unanimated content by hiding start states in CSS (.hero-title { visibility: hidden }) and animating autoAlpha from 0, or by using gsap.set before first paint; from() alone shows the end state for one frame.
  • Use fromTo when 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 });
NeedSettings
Play once on enterstart: "top 80%", toggleActions: "play none none none" or once: true
Tie progress to scrollscrub: true (or a number for smoothing seconds), ease: "none" on tweens
Hold a section while content changespin: true, end: "+=N%"
Batch reveal for many cardsScrollTrigger.batch(".card", { onEnter: (els) => gsap.from(els, {...}) })
Values depend on viewport sizeUse function values (x: () => -track.scrollWidth + innerWidth) plus invalidateOnRefresh: true
  • start/end strings 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 images width/height so 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, call ScrollTrigger.update on Lenis scroll, and set gsap.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 use autoSplit with onSplit so line breaks are recomputed after font load and resize; returning the tween lets it be cleaned up.
  • SplitText adds aria-label to 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: true wraps the element in a pin-spacer; CSS like :first-child or flex gap rules on the parent may change. Pin a wrapper, not a grid item.
  • Do not pin an element whose ancestor has transform or will-change: transform; pinning uses position: fixed and breaks inside a containing block (set pinReparent only as a last resort).
  • Animating x/y (transforms) is cheap; animating top/left/width is layout work.
  • markers: true left in production ships debug UI.
  • Each gsap.to on the same property of the same element fights the previous one; use overwrite: "auto" for hover-style re-triggers.

Verification

  • Navigate away and back several times; ScrollTrigger.getAll().length in 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.yamlgenerated from this page

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.

esc