# GSAP Core & React Integration

> **Scope:** frontend-nextjs-ui
> **Layer:** 3
> **Keywords:** gsap, animation, timeline, tween, useGSAP, greensock, transform
> **Load When:** ui-designer active, or the project animates with GSAP

**Verified against:** GSAP 3.13 + @gsap/react. Last-verified: 2026-05-20.

---

Reference guide for implementing animations in Next.js projects using GSAP and the official @gsap/react bindings. Covers core APIs, React integration via useGSAP, timeline sequencing, utility functions, performance, accessibility, and the decision boundary with Framer Motion.

---

## Section 1: When to Use GSAP vs Framer Motion

| Need | Use | Why |
|---|---|---|
| Simple enter/exit, layout animations, gestures | Framer Motion | Declarative, React-native, less boilerplate |
| ScrollTrigger (pin, scrub, parallax) | GSAP | No Framer equivalent for scroll-linked progress |
| SVG morphing, path drawing | GSAP | MorphSVG, DrawSVG plugins |
| Complex multi-step timelines | GSAP | Position parameter, labels, nested timelines |
| SplitText, scramble effects | GSAP | Premium plugins, no Framer equivalent |
| Drag with physics/inertia | GSAP | Draggable + InertiaPlugin |
| AnimatePresence (mount/unmount) | Framer Motion | GSAP has no declarative mount/unmount |

**CRITICAL RULE:** Never animate the same DOM element with both GSAP and Framer Motion simultaneously. They will fight for control of inline styles. Choose one library per element and stick with it.

---

## Section 2: Installation

```bash
npm install gsap @gsap/react
```

Since the Webflow acquisition, **GSAP and every plugin are free for commercial
use**. The public `gsap` package includes Flip, Observer, ScrollToPlugin, and the
formerly-premium plugins (SplitText, MorphSVG, DrawSVG, MotionPath, Draggable,
InertiaPlugin) — no Club GreenSock membership or private registry needed. See
`gsap-plugins.md` for the per-plugin API.

### Plugin Registration

Register plugins **once** at app init (e.g., `providers.tsx` or `layout.tsx`), not inside individual components:

```typescript
'use client';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { Flip } from 'gsap/Flip';

gsap.registerPlugin(ScrollTrigger, Flip);
```

Registering inside components causes redundant calls on every mount and risks race conditions if multiple components register different plugins.

---

## Section 3: Core Animation APIs

### Tween Methods

```typescript
'use client';
import gsap from 'gsap';

// Animate FROM current state TO target
gsap.to('.box', { x: 200, opacity: 1, duration: 0.5 });

// Animate FROM target TO current state
gsap.from('.box', { x: -200, opacity: 0, duration: 0.5 });

// Explicit start and end states
gsap.fromTo('.box',
  { x: -200, opacity: 0 },
  { x: 200, opacity: 1, duration: 0.5 }
);

// Instantly set properties (no animation)
gsap.set('.box', { x: 0, opacity: 1 });
```

### Transform Shorthand Aliases

GSAP provides shorthand aliases that map directly to CSS transforms:

| Alias | CSS Equivalent |
|---|---|
| `x`, `y` | `translateX()`, `translateY()` |
| `xPercent`, `yPercent` | `translateX(%)`, `translateY(%)` |
| `rotation` | `rotate()` |
| `scale`, `scaleX`, `scaleY` | `scale()`, `scaleX()`, `scaleY()` |
| `skewX`, `skewY` | `skewX()`, `skewY()` |

### autoAlpha

Use `autoAlpha` instead of `opacity`. It behaves identically to `opacity` but automatically sets `visibility: hidden` when the value reaches 0, removing the element from the accessibility tree and preventing invisible click targets.

```typescript
'use client';
import gsap from 'gsap';

// Element becomes visibility:hidden at autoAlpha:0
gsap.to('.overlay', { autoAlpha: 0, duration: 0.3 });

// Reveal: sets visibility:visible then animates opacity
gsap.to('.overlay', { autoAlpha: 1, duration: 0.3 });
```

---

## Section 4: React/Next.js Integration

The `useGSAP()` hook from `@gsap/react` is the canonical pattern for GSAP in React. It replaces manual `useEffect` + `gsap.context()` cleanup.

```typescript
'use client';
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';

function AnimatedComponent() {
  const containerRef = useRef<HTMLDivElement>(null);

  useGSAP(() => {
    gsap.to('.box', { x: 100, rotation: 360, duration: 1 });
  }, { scope: containerRef }); // scopes selectors to this container

  return (
    <div ref={containerRef}>
      <div className="box">Animated</div>
    </div>
  );
}
```

### Scope Refs

**ALWAYS** pass `{ scope: containerRef }` to `useGSAP`. This scopes all string selectors (e.g., `'.box'`) to descendants of that container, preventing your animations from accidentally targeting elements in other components.

### contextSafe for Event Handlers

Animations created outside the `useGSAP` callback (e.g., in click handlers) must be wrapped with `contextSafe()` to ensure proper cleanup:

```typescript
'use client';
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';

function ClickAnimated() {
  const containerRef = useRef<HTMLDivElement>(null);

  const { contextSafe } = useGSAP({ scope: containerRef });

  const handleClick = contextSafe(() => {
    gsap.to('.box', { rotation: '+=360', duration: 0.6 });
  });

  return (
    <div ref={containerRef}>
      <div className="box" onClick={handleClick}>Click me</div>
    </div>
  );
}
```

### Dependencies Array

Pass a dependencies array as the second element of the options to re-run the animation when values change:

```typescript
'use client';
import { useState, useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';

function ResponsiveAnimation() {
  const containerRef = useRef<HTMLDivElement>(null);
  const [isOpen, setIsOpen] = useState(false);

  useGSAP(() => {
    gsap.to('.panel', { height: isOpen ? 'auto' : 0, duration: 0.4 });
  }, { scope: containerRef, dependencies: [isOpen] });

  return (
    <div ref={containerRef}>
      <button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
      <div className="panel">Content</div>
    </div>
  );
}
```

### Cleanup

`useGSAP` handles cleanup automatically. When the component unmounts, all GSAP animations created inside the callback are reverted and killed. There is no need to manually call `.kill()` or `.revert()`.

**CRITICAL:** All GSAP code MUST be in `'use client'` components. GSAP cannot run server-side — it depends on the DOM and `window` object.

---

## Section 5: Timeline Sequencing

`gsap.timeline()` sequences multiple animations with precise timing control.

### Position Parameter Syntax

| Syntax | Meaning |
|---|---|
| `1` | At absolute time 1 second |
| `"+=0.5"` | 0.5s after the end of the previous animation |
| `"-=0.2"` | 0.2s before the end of the previous animation (overlap) |
| `"reveal"` | At the label named "reveal" |
| `"<"` | At the start of the previous animation |
| `">"` | At the end of the previous animation |
| `"<0.2"` | 0.2s after the start of the previous animation |

### Complete Timeline Example

```typescript
'use client';
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import { durations } from '@/lib/design-tokens';

function HeroTimeline() {
  const containerRef = useRef<HTMLDivElement>(null);

  useGSAP(() => {
    const tl = gsap.timeline({
      defaults: { duration: durations.base, ease: 'power2.out' },
    });

    tl.from('.hero-bg', { autoAlpha: 0, scale: 1.1 })
      .addLabel('reveal')
      .from('.hero-title', { y: 40, autoAlpha: 0 }, 'reveal')
      .from('.hero-subtitle', { y: 30, autoAlpha: 0 }, 'reveal+=0.15')
      .from('.hero-cta', { y: 20, autoAlpha: 0 }, '>-0.1')
      .from('.hero-badges', { autoAlpha: 0, stagger: 0.08 }, '<0.2');

    // Nested timeline
    const cardsTl = gsap.timeline({ defaults: { duration: durations.fast } });
    cardsTl.from('.card', { y: 60, autoAlpha: 0, stagger: 0.1 });

    tl.add(cardsTl, 'reveal+=0.4');
  }, { scope: containerRef });

  return (
    <div ref={containerRef}>
      <div className="hero-bg" />
      <h1 className="hero-title">Title</h1>
      <p className="hero-subtitle">Subtitle</p>
      <button className="hero-cta">Get Started</button>
      <div className="hero-badges">
        <span className="card">A</span>
        <span className="card">B</span>
        <span className="card">C</span>
      </div>
    </div>
  );
}
```

### Playback Controls

```typescript
const tl = gsap.timeline({ paused: true });

tl.play();           // play from current position
tl.pause();          // pause at current position
tl.reverse();        // play in reverse
tl.restart();        // restart from beginning
tl.progress(0.5);   // jump to 50%
tl.seek('reveal');   // jump to label
```

---

## Section 6: Utility Functions

`gsap.utils` provides composable helper functions. Omitting the last value argument returns a reusable function.

```typescript
'use client';
import gsap from 'gsap';

// Convert NodeList/selector to array
const items = gsap.utils.toArray<HTMLElement>('.items');

// Clamp value between min and max
gsap.utils.clamp(0, 100, 150);        // 100
const clamp0to100 = gsap.utils.clamp(0, 100); // reusable
clamp0to100(150);                      // 100

// Map value from one range to another
gsap.utils.mapRange(0, 1, 0, 100, 0.5);  // 50
const normalize = gsap.utils.mapRange(0, 1, 0, 100); // reusable

// Snap to nearest grid value
gsap.utils.snap(10, 23);              // 20
const snapTo5 = gsap.utils.snap(5);   // reusable

// Interpolate between two values
gsap.utils.interpolate(0, 100, 0.5);  // 50

// Compose utility functions into a pipeline
const process = gsap.utils.pipe(
  gsap.utils.clamp(0, 100),
  gsap.utils.snap(5)
);
process(103); // 100 (clamped), then 100 (snapped)

// Scoped selector (useful in components)
const q = gsap.utils.selector(containerRef.current);
gsap.to(q('.title'), { y: -20 });
```

---

## Section 7: Performance

- **Animate ONLY transform properties** (`x`, `y`, `scale`, `rotation`) and `opacity` — these use the GPU compositor and skip layout/paint.
- **AVOID layout-triggering properties** (`width`, `height`, `top`, `left`, `margin`, `padding`) — they cause full layout recalculation.
- Apply `will-change: transform` ONLY on elements that are actively animating, and remove it after the animation completes.
- Use `gsap.quickTo()` for high-frequency updates such as mouse followers and cursor tracking:

```typescript
'use client';
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';

function CursorFollower() {
  const cursorRef = useRef<HTMLDivElement>(null);

  useGSAP(() => {
    const xTo = gsap.quickTo(cursorRef.current, 'x', { duration: 0.3, ease: 'power3' });
    const yTo = gsap.quickTo(cursorRef.current, 'y', { duration: 0.3, ease: 'power3' });

    const handleMouseMove = (e: MouseEvent) => {
      xTo(e.clientX);
      yTo(e.clientY);
    };

    window.addEventListener('mousemove', handleMouseMove);
    return () => window.removeEventListener('mousemove', handleMouseMove);
  });

  return <div ref={cursorRef} className="cursor-dot" />;
}
```

- Use the `stagger` property instead of creating individual tweens for each element.
- Batch DOM reads before writes to prevent layout thrashing.
- Kill off-screen animations and debounce `ScrollTrigger.refresh()` on resize.

---

## Section 8: Accessibility

**MANDATORY:** Every animated component must respect `prefers-reduced-motion`.

```typescript
'use client';
import { useRef } from 'react';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';

// Option A: gsap.matchMedia (recommended)
function ScrollHero() {
  const containerRef = useRef<HTMLDivElement>(null);

  useGSAP(() => {
    gsap.matchMedia().add('(prefers-reduced-motion: no-preference)', () => {
      // Animations here are automatically reverted when media query stops matching
      gsap.to('.hero', { y: -50, scrollTrigger: { scrub: true } });
    });
  }, { scope: containerRef });

  return (
    <div ref={containerRef}>
      <div className="hero">Content</div>
    </div>
  );
}

// Option B: manual check
function ManualCheckExample() {
  const containerRef = useRef<HTMLDivElement>(null);

  useGSAP(() => {
    const prefersReducedMotion = window.matchMedia(
      '(prefers-reduced-motion: reduce)'
    ).matches;

    if (prefersReducedMotion) {
      gsap.set('.hero', { opacity: 1 }); // skip to final state
      return;
    }

    gsap.from('.hero', { y: 40, autoAlpha: 0, duration: 0.6 });
  }, { scope: containerRef });

  return (
    <div ref={containerRef}>
      <div className="hero">Content</div>
    </div>
  );
}
```

When reduced motion is active:
- Skip all transform animations (`x`, `y`, `scale`, `rotation`).
- Keep opacity-only fades — they do not cause vestibular issues.
- Use `gsap.set()` to jump to the final visual state so content is still visible.

---

## Section 9: Design Token Bridge

Import from the same `@/lib/design-tokens` module that Framer Motion uses to maintain consistency across animation libraries:

```typescript
'use client';
import gsap from 'gsap';
import { durations, easings } from '@/lib/design-tokens';

gsap.to('.element', {
  duration: durations.base, // 0.3
  ease: `cubic-bezier(${easings.standard.join(',')})`,
});
```

Alternatively, use GSAP's built-in easing strings when the design token bridge is not needed:

| GSAP Ease | Behavior |
|---|---|
| `"power2.out"` | Standard deceleration |
| `"power2.inOut"` | Smooth start and end |
| `"back.out(1.7)"` | Slight overshoot |
| `"elastic.out(1, 0.3)"` | Spring-like bounce |
| `"none"` | Linear (no easing) |

> **Rule:** Never hardcode duration numbers. Import from `@/lib/design-tokens`.

---

## Section 10: Anti-patterns

| Anti-pattern | Correct approach |
|---|---|
| Using useEffect instead of useGSAP | Always use `useGSAP()` from `@gsap/react` — handles cleanup automatically |
| Forgetting scope ref | Always pass `{ scope: containerRef }` to useGSAP — prevents targeting elements in other components |
| No cleanup | useGSAP handles this; for manual `gsap.context()`, call `ctx.revert()` in cleanup |
| Animating width/height/top/left | Use `x`, `y`, `scale`, `rotation` (GPU-composited) |
| No prefers-reduced-motion check | Wrap animations in `gsap.matchMedia()` or check manually |
| Mixing GSAP + Framer Motion on same element | Choose one per element — they fight for inline styles |
| Registering plugins inside components | Register once at app init (`layout.tsx` or `providers.tsx`) |
| Using GSAP for simple enter/exit | Use Framer Motion — it is declarative and handles mount/unmount |
| Hardcoded durations/easings | Import from `@/lib/design-tokens` |
| Creating animations before DOM exists | useGSAP runs after mount — never animate in render body |

---

*MORPH-SPEC by Polymorphism Tech*
