
gsap-core
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
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.