Sign inGet started

Helps create JavaScript animations with GSAP's core API, including tweens, easing, stagger, and responsive/reduced-motion animation in any framework or vanilla JS.

Lift+25ppscreen tier · directional
87
Trustsecurity · quality · license · upkeep
Scan Saferisk 0/100
LicenseMITserved via MCP
Skill-matched tasks (generated, public + held-out) · glm-5.3-flash@cf
+25pp95% CI [0, 63]unverified · screen tier
baseline 50% → skill 75% pass
trigger rate 100% · 24 trials

A/B outputs — median trial per arm

Same task, same model: without the skill (left) vs with it (right).

gsap-core--m1
baseline · 0.52
```html
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script>
  window.addEventListener('load', function () {
    gsap.set('#box', { x: -200 });
    gsap.to('#box', { x: 300, duration: 1.5, ease: 'bounce.out' });
  });
</script>
```
with skill · 0.85
```html
<div id="box" style="width:80px;height:80px;background:#4f8"></div>

<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script>
  gsap.fromTo("#box", { x: -200 }, { x: 300, duration: 1.5, ease: "bounce.out" });
</script>
```

`fromTo()` applies the off-screen start position immediately (no flash of the box at its original spot), then animates to `x: 300` with a bounce ease over 1.5 seconds.
gsap-core--m2
baseline · 0.91
Here's the component with GSAP mount/unmount animations using hooks:

```jsx
import { useRef, useLayoutEffect } from "react";
import gsap from "gsap";

function TodoList({ items, onClose }) {
  const listRef = useRef(null);

  // Mount animation: fade in + slide up
  useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      gsap.fromTo(
        listRef.current,
        { opacity: 0, y: 40 },
        { opacity: 1, y: 0, duration: 0.5, ease: "power2.out" }
      );
    }, listRef);
    return () => ctx.revert(); // cleanup
  }, []);

  // Unmount animation: fade out + slide down, then call onClose
  const handleClose = () => {
    gsap.to(listRef.current, {
      opacity: 0,
      y: 40,
      duration: 0.4,
      ease: "power2.in",
      onComplete: onClose, // actually unmount after animation finishes
    });
  };

  return (
    <div>
      <ul ref={listRef}>
        {items.map((i) => (
          <li key={i.id}>{i.text}</li>
        ))}
      </ul>
      <button onClick={handleClose}>Close</button>
    </div>
  );
}
```

Key points:

- **`useLayoutEffect`** runs before the browser paints, so the element never flashes in its unanimated state.
- **`gsap.fromTo`** sets the starting state (`opacity: 0, y: 40`) and animates to the final state, giving you the "fade + slide up" on mount.
- **`gsap.context` + `ctx.revert()`** is the React 18–recommended pattern — it scopes selectors and cleanly reverts animations on cleanup (important with StrictMode's double-invoked effects).
- **Exit animation trick:** since React unmounts immediately when the parent removes the component, you can't animate the unmount from inside after the fact. Instead, intercept the close action: play the fade/slide-down tween, and call the real `onClose` in `onComplete` so the unmount happens *after* the animation.

If `onClose` is triggered externally (parent state change you don't control), you'd need an exit-animation library like `react-transition-group` or `framer-motion`'s `AnimatePresence` to delay the unmount — GSAP alone can't pause React's removal.
with skill · 0.85
Here's the pattern. The key insight: React unmounts immediately when the parent removes it, so the exit animation has to happen **inside** the component — animate out first, then call `onClose()` in the tween's `onComplete`.

```jsx
import { useRef, useLayoutEffect } from "react";
import gsap from "gsap";

function TodoList({ items, onClose }) {
  const listRef = useRef(null);
  const hasClosed = useRef(false); // guard against double-calls

  // Mount animation: fade in + slide up
  useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      gsap.from(listRef.current, {
        autoAlpha: 0,   // opacity + visibility (better than opacity alone)
        y: 24,          // slide up from 24px below
        duration: 0.5,
        ease: "power2.out",
      });
    }, listRef);
    return () => ctx.revert(); // cleanup on unmount
  }, []);

  // Exit animation, then actually unmount
  const handleClose = () => {
    if (hasClosed.current) return;
    hasClosed.current = true;

    gsap.to(listRef.current, {
      autoAlpha: 0,
      y: 24,          // slide down as it fades
      duration: 0.4,
      ease: "power2.in",
      onComplete: onClose, // remove from parent AFTER animation
    });
  };

  return (
    <div>
      <ul ref={listRef}>
        {items.map((i) => (
          <li key={i.id}>{i.text}</li>
        ))}
      </ul>
      <button onClick={handleClose}>Close</button>
    </div>
  );
}
```

Usage in the parent — just conditionally render it; the component handles its own exit timing:

```jsx
function App() {
  const [open, setOpen] = useState(true);
  return open ? (
    <TodoList items={todos} onClose={() => setOpen(false)} />
  ) : null;
}
```

### Notes

- **`useLayoutEffect`** runs before paint, so `gsap.from()` never flashes the visible list before animating. (`from()` also has `immediateRender: true` by default, which helps here.)
- **`autoAlpha`** instead of `opacity`: at 0 it also sets `visibility: hidden`, so the invisible list can't intercept clicks.
- **`gsap.context(..., listRef)`** scopes selectors and gives you clean teardown via `ctx.revert()` — the recommended React cleanup pattern.
- **The exit trick**: since React removes the component synchronously, you can't animate "on unmount" directly. Instead, intercept the close action, play the out-tween, and call the real `onClose` in `onComplete`. If the parent might unmount the list for other reasons (e.g. route change), the exit animation will be skipped — for full control in those cases, consider a library like `react-transition-group` or GSAP's Flip plugin.
- If you want the `<li>` items to stagger in, animate the children instead:

```jsx
gsap.from(listRef.current.children, {
  autoAlpha: 0,
  y: 16,
  stagger: 0.08,
  duration: 0.4,
  ease: "power2.out",
});
```
gsap-core--m3
baseline · 0.65
```html
<script>
  gsap.matchMedia().add(
    {
      isMobile: "(max-width: 767px)",
      isDesktop: "(min-width: 768px)",
      reduceMotion: "(prefers-reduced-motion: reduce)",
    },
    (context) => {
      const { isMobile, isDesktop, reduceMotion } = context.conditions;
      const h1 = document.querySelector(".hero h1");
      const button = document.querySelector(".hero button");

      if (reduceMotion) {
        // Skip all animation
        gsap.set([h1, button], { clearProps: "all" });
        return;
      }

      if (isMobile) {
        gsap.from(h1, { x: -40, duration: 0.6, ease: "power2.out" });
        gsap.from(button, { y: 20, duration: 0.6, ease: "power2.out", delay: 0.15 });
      }

      if (isDesktop) {
        gsap.from(h1, { scale: 0.8, duration: 0.6, ease: "power2.out" });
        gsap.from(button, { y: 60, duration: 0.6, ease: "power2.out", delay: 0.15 });
      }
    }
  );
</script>
```

`gsap.matchMedia()` automatically re-runs the callback when the viewport crosses the 768px breakpoint, and the `prefers-reduced-motion` condition ensures no animation runs for users who opt out.
with skill · 0.65
```javascript
const mm = gsap.matchMedia();

mm.add(
  {
    isMobile: "(max-width: 767px)",
    isDesktop: "(min-width: 768px)",
    reduceMotion: "(prefers-reduced-motion: reduce)"
  },
  (context) => {
    const { isMobile, reduceMotion } = context.conditions;

    if (reduceMotion) return; // skip all animation

    if (isMobile) {
      gsap.from(".hero h1", { x: -40, duration: 0.6, ease: "power2.out" });
      gsap.from(".hero button", { y: 20, duration: 0.6, ease: "power2.out" });
    }

    if (isDesktop) {
      gsap.from(".hero h1", { scale: 0.8, duration: 0.6, ease: "power2.out" });
      gsap.from(".hero button", { y: 60, duration: 0.6, ease: "power2.out" });
    }
  }
);
```

Notes:
- `gsap.matchMedia()` automatically reverts animations when the viewport crosses the 768px breakpoint or the reduced-motion preference changes.
- The `reduceMotion` condition short-circuits setup entirely, so users who prefer reduced motion see no animation at all.
- `gsap.from()` animates from the given values to the elements' natural state (x: 0, y: 0, scale: 1).
gsap-core--m4
baseline · 0.67
```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script>
  // Global defaults for all tweens
  gsap.defaults({ duration: 0.8, ease: "power2.out" });

  const cards = gsap.utils.toArray(".card");

  // Staggered entrance
  gsap.from(cards, {
    y: 80,
    opacity: 0,
    stagger: 0.15
  });

  // Total time for the stagger to finish: 0.8 + (5 - 1) * 0.15 = 1.4s
  const staggerEnd = 0.8 + (cards.length - 1) * 0.15;

  // After the stagger, rotate each card's inner <span>,
  // delayed by its card's data-speed value
  cards.forEach((card) => {
    const speed = parseFloat(card.dataset.speed) || 0;
    gsap.to(card.querySelector("span"), {
      rotation: 360,
      delay: staggerEnd + speed
    });
  });
</script>
```

Notes:
- `gsap.defaults()` sets `duration: 0.8` and `ease: "power2.out"` for every tween created afterward, including the span rotations.
- The span rotation starts once the last card finishes animating in, plus its own `data-speed` delay.
with skill · 0.67
```javascript
gsap.defaults({ duration: 0.8, ease: "power2.out" });

const cards = gsap.utils.toArray(".card");

// Staggered entrance: each card starts 0.15s after the previous
const entrance = gsap.from(cards, {
  y: 80,
  autoAlpha: 0,
  stagger: 0.15
});

// After the stagger completes, spin each card's <span>
entrance.eventCallback("onComplete", () => {
  cards.forEach((card) => {
    gsap.to(card.querySelector("span"), {
      rotation: 360,
      delay: parseFloat(card.dataset.speed)
    });
  });
});
```

Notes:
- `gsap.defaults()` sets the global `duration: 0.8` and `ease: "power2.out"` for all future tweens, so the span rotation picks them up automatically.
- `autoAlpha` fades in and also handles `visibility`, so cards don't block clicks while hidden.
- `entrance.eventCallback("onComplete", ...)` fires after the last card finishes (i.e., after the full stagger), then each span rotates with a delay equal to its card's `data-speed`.
gsap-core--m5
baseline · 1.00
```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>
<script>
  const box = document.getElementById('box');

  box.addEventListener('click', () => {
    gsap.to(box, {
      x: 300,          // move 300px to the right
      rotation: 180,   // rotate 180 degrees
      duration: 1,
      ease: 'bounce.out'
    });
  });
</script>
```

Notes:
- GSAP is a vanilla-JS tween library — no jQuery needed.
- `bounce.out` overshoots/settles at the end; use `bounce.inOut` if you want the bounce on both ends.
- If you want repeated clicks to toggle back and forth instead of continuing right, swap `gsap.to` for `gsap.fromTo(box, {x: 0, rotation: 0}, {...})` or use `gsap.to(box, {x: "+=300", ...})`.
with skill · 1.00
```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3/dist/gsap.min.js"></script>

<script>
  document.getElementById("box").addEventListener("click", () => {
    gsap.to("#box", {
      x: "+=300",        // move 300px right (relative, so repeat clicks keep working)
      rotation: "+=180", // rotate 180 degrees
      duration: 1,
      ease: "bounce.out",
      overwrite: "auto"  // cleanly restart if clicked mid-animation
    });
  });
</script>
```

Notes:
- `x` and `rotation` are GSAP's transform aliases for `translateX` and `rotate` — they animate via GPU-friendly transforms, not layout properties.
- `bounce.out` gives the classic "settle with bounces at the end" feel; use `bounce.inOut` if you want bounces on both ends.
- The `+=` relative values mean each click moves it another 300px and rotates another 180°. If you want it to always go to the same spot instead, use absolute values: `x: 300, rotation: 180`.
Limitations
  • Agent noise: single-model results vary run to run; the confidence interval reflects task-level variance.
  • Screen tier (1 trial/arm) is a directional gate, not a verified claim.
  • Results are for the SkillzBench Reference Agent on glm-5.3-flash@cf and do not necessarily transfer to other agents or models.
  • Task coverage: Skill-matched tasks (generated, public + held-out) only; a skill can shine elsewhere.