Sign inGet started

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
baseline 100% → skill 75% pass
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.