
gsap-react
Animates React components using GSAP with proper cleanup and context management.
Lift-25ppscreen tier · directional
86
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 [-75, 0]unverified · screen tier
trigger rate 100% · 12 trials
A/B outputs — median trial per arm
Same task, same model: without the skill (left) vs with it (right).
gsap-react--m1
baseline · 0.85
Here's how to do it with the `useGSAP` hook from `@gsap/react`:
```bash
npm install gsap @gsap/react
```
```jsx
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP);
function FadeInButton() {
const buttonRef = useRef(null);
useGSAP(() => {
gsap.from(buttonRef.current, {
opacity: 0,
y: 40, // slide up from 40px below
duration: 0.8,
ease: "power2.out",
});
}, { scope: buttonRef }); // optional: scopes selector text to this component
return (
<div>
<button ref={buttonRef} className="my-button">
Click me
</button>
</div>
);
}
export default FadeInButton;
```
Key points:
- **`useGSAP` runs on mount** by default, so the animation plays when the component appears — no `useEffect` boilerplate needed.
- **Cleanup is automatic**: any animations created inside the hook are reverted when the component unmounts.
- **`gsap.from`** animates *from* the given values (opacity 0, y: 40) back to the element's natural state.
- **`scope`** ensures any selector-based animations (e.g. `gsap.from(".my-button", ...)`) only target elements inside this component — useful to avoid collisions in a larger app.
- If you want the animation to run again on dependency changes, pass a dependencies array: `useGSAP(fn, { dependencies: [someValue] })`.
If you prefer class-based targeting instead of a ref:
```jsx
useGSAP(() => {
gsap.from(".my-button", { opacity: 0, y: 40, duration: 0.8 });
}, { scope: containerRef });
```
(then put `ref={containerRef}` on a wrapping element).with skill · 0.82
Install the packages first:
```bash
npm install gsap @gsap/react
```
```jsx
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP);
export default function FadeUpButton() {
const containerRef = useRef(null);
const buttonRef = useRef(null);
useGSAP(
() => {
gsap.from(buttonRef.current, {
opacity: 0,
y: 40,
duration: 0.8,
ease: "power2.out",
});
},
{ scope: containerRef } // scopes selectors/cleanup to this component
);
return (
<div ref={containerRef}>
<button ref={buttonRef} className="my-button">
Click me
</button>
</div>
);
}
```
Key points:
- `gsap.from()` animates from `opacity: 0` / `y: 40` back to the element's natural state — perfect for mount animations.
- The `scope` option ensures any selector-based targeting stays inside this component, and cleanup (reverting the animation) happens automatically on unmount — no manual `useEffect` cleanup needed.
- Using a ref for the button targets the actual DOM node, which is safer than a bare selector string.gsap-react--m2
baseline · 1.00
Here's a clean pattern using `useRef`, a scoped selector, and `gsap.context()` for safe cleanup:
```jsx
import { useRef, useEffect } from "react";
import gsap from "gsap";
function ItemList({ items }) {
const containerRef = useRef(null);
useEffect(() => {
const ctx = gsap.context(() => {
gsap.from(".item", {
opacity: 0,
scale: 0.8,
y: 20,
duration: 0.5,
ease: "power2.out",
stagger: 0.1,
});
}, containerRef); // scope: selectors only match inside this ref
return () => ctx.revert(); // cleanup: reverts animations on unmount
}, [items]);
return (
<ul ref={containerRef}>
{items.map((item) => (
<li key={item.id} className="item">
{item.label}
</li>
))}
</ul>
);
}
export default ItemList;
```
Key points:
- **`gsap.context(fn, containerRef)`** scopes selector text like `".item"` to only elements inside the container — no global DOM queries, so it's safe even if multiple lists exist on the page.
- **`ctx.revert()`** in the cleanup restores elements to their original state and kills the tweens, preventing leaks and double-animation issues in React 18 StrictMode (which double-invokes effects in dev).
- **`gsap.from`** animates *from* the given values to the natural state, which is ideal for entrance animations.
- **`stagger: 0.1`** offsets each item by 100ms for the staggered effect. You can also use `stagger: { each: 0.1, from: "start" }` for more control.
If items can be added/removed dynamically, re-run the effect on `items` change (as above) — the context revert ensures old animations are cleaned up before new ones run.with skill · 1.00
Here's the recommended approach using `useGSAP()` from `@gsap/react` (it handles cleanup automatically), with a container ref as the scope:
```jsx
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP);
function ItemList({ items }) {
const containerRef = useRef(null);
useGSAP(
() => {
// Scoped to containerRef, so ".item" only matches inside this component
gsap.from(".item", {
opacity: 0,
scale: 0.8,
duration: 0.5,
ease: "power2.out",
stagger: 0.1, // 0.1s between each item
});
},
{ scope: containerRef }
);
return (
<ul ref={containerRef}>
{items.map((item) => (
<li key={item.id} className="item">
{item.label}
</li>
))}
</ul>
);
}
```
If you'd rather use plain `useEffect()` with `gsap.context()` (no `@gsap/react` installed), the equivalent is:
```jsx
import { useRef, useEffect } from "react";
import gsap from "gsap";
function ItemList({ items }) {
const containerRef = useRef(null);
useEffect(() => {
const ctx = gsap.context(() => {
gsap.from(".item", {
opacity: 0,
scale: 0.8,
duration: 0.5,
ease: "power2.out",
stagger: 0.1,
});
}, containerRef); // scope: selectors limited to this component
return () => ctx.revert(); // cleanup: kills tweens, reverts inline styles
}, []);
return (
<ul ref={containerRef}>
{items.map((item) => (
<li key={item.id} className="item">
{item.label}
</li>
))}
</ul>
);
}
```
Key points:
- **Scope**: passing `containerRef` to `useGSAP`/`gsap.context()` means `.item` only matches elements inside this component — no accidental matches elsewhere.
- **Cleanup**: `useGSAP` reverts automatically on unmount; with `gsap.context()` you must call `ctx.revert()` in the effect's return.
- **Re-animating on list changes**: if `items` can change and you want the animation to re-run, add `dependencies: [items.length]` and `revertOnUpdate: true` to the `useGSAP` config (or add `[items.length]` to the `useEffect` deps).
- **`gsap.from` vs `gsap.to`**: `from` animates from the given values to the current state, which is ideal for entrance animations. If items may re-render mid-animation, consider `gsap.fromTo` for deterministic behavior.gsap-react--m3
baseline · 0.85
# Cleaning Up GSAP Animations with `useGSAP`
The good news: the `useGSAP()` hook from `@gsap/react` **already handles cleanup automatically**. Any animations/tweens/timelines created inside the hook's callback are killed and reverted when the component unmounts. Most "animations keep running after unmount" errors come from creating GSAP animations *outside* the hook — e.g., in `useEffect`, event handlers, or module scope.
## Correct pattern
```jsx
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP);
function MyComponent() {
const container = useRef(null);
useGSAP(() => {
// ✅ Everything created here is auto-cleaned on unmount
gsap.to(".box", { x: 100, duration: 1 });
gsap.to(".circle", { rotation: 360, duration: 2 });
}, { scope: container }); // scope limits selector text to this component
return (
<div ref={container}>
<div className="box" />
<div className="circle" />
</div>
);
}
```
## Key points
1. **Create animations inside the `useGSAP` callback** — never in `useEffect` or event handlers, or you're responsible for cleanup yourself.
2. **Use `scope: container`** so selector strings like `".box"` only match elements inside your component (avoids accidentally animating other components' elements).
3. **Dependencies**: pass a dependency array like a normal hook to re-run animations when values change — cleanup still happens automatically between runs:
```jsx
useGSAP(() => {
gsap.from(".item", { y: 20, opacity: 0, stagger: 0.1 });
}, { scope: container, dependencies: [items.length] });
```
4. **Animations created outside the hook** (e.g., in a click handler) need manual cleanup:
```jsx
const tweenRef = useRef();
const handleClick = () => {
tweenRef.current = gsap.to(".box", { x: 200 });
};
// Clean up manually if needed
useEffect(() => () => {
tweenRef.current?.kill();
}, []);
```
5. If you still see errors after unmount, check for:
- Timelines stored in refs and played later
- `gsap.ticker.add(...)` or `gsap.delayedCall(...)` calls — these need explicit `gsap.ticker.remove()` / `kill()` since the hook can't track them
- Animations on elements outside the component's scope
If you share the specific error message or your component code, I can pinpoint exactly where the leak is coming from.with skill · 0.85
The good news: `useGSAP()` already handles cleanup automatically on unmount — if animations are still running after unmount, they were likely created *outside* the hook (e.g., in event handlers). Here's the correct pattern:
## Standard pattern
```jsx
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP);
function MyComponent() {
const containerRef = useRef(null);
useGSAP(() => {
// Everything created here is tracked and auto-reverted on unmount
gsap.to(".box", { x: 100 });
gsap.from(".item", { opacity: 0, stagger: 0.1 });
}, { scope: containerRef }); // scope limits selectors to this component
return (
<div ref={containerRef}>
<div className="box" />
<div className="item" />
</div>
);
}
```
Key points:
- **All GSAP code goes inside the `useGSAP()` callback** — animations, timelines, ScrollTriggers created there are automatically killed/reverted on unmount.
- **Pass a `scope`** so selector strings like `".box"` only match elements inside your component.
## The common culprit: animations created in event handlers
If you create animations in callbacks that run *after* the hook executes (click handlers, `onComplete`, etc.), they aren't tracked. Wrap them with `contextSafe`:
```jsx
useGSAP((context, contextSafe) => {
const onClick = contextSafe(() => {
gsap.to(boxRef.current, { rotation: 180 }); // ✅ now tracked & cleaned up
});
boxRef.current.addEventListener("click", onClick);
return () => {
boxRef.current.removeEventListener("click", onClick); // remove listeners too
};
}, { scope: containerRef });
```
## If you're using plain `useEffect` instead
Always revert a context in cleanup:
```jsx
useEffect(() => {
const ctx = gsap.context(() => {
gsap.to(".box", { x: 100 });
}, containerRef);
return () => ctx.revert(); // kills tweens, ScrollTriggers, reverts inline styles
}, []);
```
## If animations should re-run when dependencies change
```jsx
useGSAP(() => {
gsap.to(".box", { x: endX });
}, { dependencies: [endX], scope: containerRef, revertOnUpdate: true });
```
**Summary:** move all animation creation inside `useGSAP()`, use `contextSafe` for anything created in later callbacks, and remove any manual event listeners in the hook's cleanup return. That guarantees nothing touches detached DOM nodes after unmount.gsap-react--m4
baseline · 0.85
Here's a complete, SSR-safe setup for Next.js App Router with GSAP's `useGSAP` hook and a scroll-triggered hero animation.
## 1. Install dependencies
```bash
npm install gsap @gsap/react
```
## 2. Mark the component as a Client Component
GSAP animations must run in the browser, so the component needs `"use client"`. The `useGSAP` hook from `@gsap/react` handles cleanup automatically and avoids the classic `useEffect` double-run issues in React 18 Strict Mode.
## 3. Complete example
```tsx
// app/components/Hero.tsx
"use client";
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import { ScrollTrigger } from "gsap/ScrollTrigger";
gsap.registerPlugin(useGSAP, ScrollTrigger);
export default function Hero() {
const container = useRef<HTMLDivElement>(null);
useGSAP(
() => {
// Initial state — set inside useGSAP so it runs only on the client,
// after hydration, avoiding SSR/hydration mismatch warnings.
gsap.set(".hero-title, .hero-sub", { autoAlpha: 0, y: 40 });
// Animate when the hero scrolls into view
gsap.to(".hero-title, .hero-sub", {
autoAlpha: 1,
y: 0,
duration: 1,
stagger: 0.2,
ease: "power3.out",
scrollTrigger: {
trigger: container.current,
start: "top 80%", // when top of hero hits 80% of viewport height
toggleActions: "play none none none",
once: true, // play only the first time
},
});
},
{ scope: container } // scopes selector text to this component
);
return (
<section ref={container} className="hero">
<h1 className="hero-title">Welcome to my site</h1>
<p className="hero-sub">Scroll-triggered, SSR-safe animation.</p>
</section>
);
}
```
```tsx
// app/page.tsx (Server Component — no changes needed)
import Hero from "./components/Hero";
export default function Page() {
return (
<main>
<Hero />
{/* rest of your page */}
</main>
);
}
```
## Why this avoids SSR/hydration problems
1. **No animation logic during SSR** — `useGSAP` only runs its callback on the client after hydration, so the server-rendered HTML and the first client render match exactly.
2. **Initial hidden state set via `gsap.set`, not inline styles or CSS classes** — if you set `opacity: 0` in your JSX/CSS, the element stays invisible if JS fails or is slow. Using `gsap.set` inside `useGSAP` means the content is visible by default and only hidden right before animating.
3. **`scope: container`** — selector strings like `".hero-title"` are scoped to the component's DOM, so they can't accidentally match elements elsewhere on the page.
4. **Automatic cleanup** — `useGSAP` reverts/kills animations and ScrollTriggers when the component unmounts, which matters with App Router's client-side navigation.
5. **`gsap.registerPlugin` at module level** — safe because it's guarded by `"use client"` and runs identically on client loads.
## Optional: avoid a flash of visible content
If the hero is above the fold and you want zero flash before JS loads, add a tiny inline script or a CSS class that hides it, then remove it in `useGSAP`:
```css
/* globals.css */
.hero-title, .hero-sub { visibility: hidden; }
```
```tsx
useGSAP(() => {
gsap.set(".hero-title, .hero-sub", { autoAlpha: 0, y: 40 }); // autoAlpha manages visibility too
// ...animation as before
});
```
`autoAlpha: 1` sets `visibility: visible` and `opacity: 1`, so the CSS-hidden state gets cleanly overridden. Just be aware this trades "content hidden if JS fails" for "no flash" — pick based on your needs.
## Tips
- If the hero is already in view on load, ScrollTrigger fires immediately — that's usually what you want.
- For scroll-linked (scrubbed) animations instead of a one-shot trigger, add `scrub: true` to the `scrollTrigger` config.
- If you navigate between pages and animations don't re-trigger, call `ScrollTrigger.refresh()` after route changes (often handled automatically, but worth knowing).with skill · 0.00
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.