Claude
Skills
Sign in
Back

gsap-animation

Included with Lifetime
$97 forever

GSAP + Remotion integration for professional motion graphics video production. Timeline orchestration, text splitting, SVG morphing, advanced easing, and reusable effect presets.

Image & Video

What this skill does


## When to use

Use this skill when creating Remotion video compositions that need **GSAP's advanced animation capabilities** beyond Remotion's built-in `interpolate()` and `spring()`.

**Use GSAP when you need:**
- Complex timeline orchestration (nesting, labels, position parameters like `"-=0.5"`)
- Text splitting animation (SplitText: chars/words/lines with mask reveals)
- SVG shape morphing (MorphSVG), stroke drawing (DrawSVG), path-following (MotionPath)
- Advanced easing (CustomEase from SVG paths, RoughEase, SlowMo, CustomBounce, CustomWiggle)
- Stagger with grid, center/edges distribution
- Character scramble/decode effects (ScrambleText)
- Reusable named effects via `gsap.registerEffect()`

**Use Remotion native `interpolate()` when:**
- Simple single-property animations (fade, slide, scale) -- do NOT use GSAP for these
- Numeric counters/progress bars -- pure math, no timeline needed
- Standard easing curves
- Spring physics (`spring()`)

**GSAP Licensing:** All plugins are **100% free** since Webflow's 2024 acquisition (SplitText, MorphSVG, DrawSVG, etc.).

---

## Setup

```bash
# In a Remotion project
npm install gsap
```

```tsx
// src/gsap-setup.ts -- import once at entry point
import gsap from 'gsap';
import { SplitText } from 'gsap/SplitText';
import { MorphSVGPlugin } from 'gsap/MorphSVGPlugin';
import { DrawSVGPlugin } from 'gsap/DrawSVGPlugin';
import { MotionPathPlugin } from 'gsap/MotionPathPlugin';
import { ScrambleTextPlugin } from 'gsap/ScrambleTextPlugin';
import { CustomEase } from 'gsap/CustomEase';
import { CustomBounce } from 'gsap/CustomBounce';
import { CustomWiggle } from 'gsap/CustomWiggle';

gsap.registerPlugin(
  SplitText, MorphSVGPlugin, DrawSVGPlugin, MotionPathPlugin,
  ScrambleTextPlugin, CustomEase, CustomBounce, CustomWiggle,
);

export { gsap };
```

---

## Core Hook: useGSAPTimeline

The bridge between GSAP and Remotion. Creates a paused timeline, seeks it to `frame / fps` every frame.

```tsx
import { useCurrentFrame, useVideoConfig } from 'remotion';
import gsap from 'gsap';
import { useRef, useEffect } from 'react';

function useGSAPTimeline(
  buildTimeline: (tl: gsap.core.Timeline, container: HTMLDivElement) => void
) {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const containerRef = useRef<HTMLDivElement>(null);
  const tlRef = useRef<gsap.core.Timeline | null>(null);

  useEffect(() => {
    if (!containerRef.current) return;
    const ctx = gsap.context(() => {
      const tl = gsap.timeline({ paused: true });
      buildTimeline(tl, containerRef.current!);
      tlRef.current = tl;
    }, containerRef);
    return () => { ctx.revert(); tlRef.current = null; };
  }, []);

  useEffect(() => {
    if (tlRef.current) tlRef.current.seek(frame / fps);
  }, [frame, fps]);

  return containerRef;
}
```

**For SplitText (needs font loading):**

```tsx
import { delayRender, continueRender } from 'remotion';

function useGSAPWithFonts(
  buildTimeline: (tl: gsap.core.Timeline, container: HTMLDivElement) => void
) {
  const frame = useCurrentFrame();
  const { fps } = useVideoConfig();
  const containerRef = useRef<HTMLDivElement>(null);
  const tlRef = useRef<gsap.core.Timeline | null>(null);
  const [handle] = useState(() => delayRender());

  useEffect(() => {
    document.fonts.ready.then(() => {
      if (!containerRef.current) return;
      const ctx = gsap.context(() => {
        const tl = gsap.timeline({ paused: true });
        buildTimeline(tl, containerRef.current!);
        tlRef.current = tl;
      }, containerRef);
      continueRender(handle);
      return () => { ctx.revert(); };
    });
  }, []);

  useEffect(() => {
    if (tlRef.current) tlRef.current.seek(frame / fps);
  }, [frame, fps]);

  return containerRef;
}
```

---

## 1. Text Animations

### SplitText Reveal (chars/words/lines)

```tsx
const TextReveal: React.FC<{ text: string }> = ({ text }) => {
  const containerRef = useGSAPWithFonts((tl, container) => {
    const split = SplitText.create(container.querySelector('.heading')!, {
      type: 'chars,words,lines', mask: 'lines',
    });
    tl.from(split.chars, {
      y: 100, opacity: 0, duration: 0.6, stagger: 0.03, ease: 'power2.out',
    });
  });

  return (
    <AbsoluteFill style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <div ref={containerRef}>
        <h1 className="heading" style={{ fontSize: 80, fontWeight: 'bold' }}>{text}</h1>
      </div>
    </AbsoluteFill>
  );
};
```

**Patterns:**
| Pattern | SplitText Config | Animation |
|---------|-----------------|-----------|
| Line reveal | `type: "lines", mask: "lines"` | `from lines: { y: "100%" }` |
| Char cascade | `type: "chars"` | `from chars: { y: 50, opacity: 0, rotationX: -90 }` |
| Word scale | `type: "words"` | `from words: { scale: 0, opacity: 0 }` |
| Char + color | `type: "chars"` | `.from(chars, { y: 50 }).to(chars, { color: "#f00" })` |

### ScrambleText (decode effect)

> **Determinism warning:** ScrambleText uses internal random character selection. Use `--concurrency=1` when rendering to guarantee frame-perfect reproducibility across renders.

```tsx
const containerRef = useGSAPTimeline((tl, container) => {
  tl.to(container.querySelector('.text')!, {
    duration: 2,
    scrambleText: { text: 'DECODED', chars: '01', revealDelay: 0.5, speed: 0.3 },
  });
});
```

**Char sets:** `"upperCase"`, `"lowerCase"`, `"upperAndLowerCase"`, `"01"`, or custom string.

### Text Highlight Box

Colored rectangles scale in behind specific words. Uses SplitText for word-level positioning, then absolutely-positioned `<div>` boxes at lower z-index.

```tsx
const TextHighlightBox: React.FC<{
  text: string;
  highlights: Array<{ wordIndex: number; color: string }>;
  highlightDelay?: number;
  highlightStagger?: number;
}> = ({ text, highlights, highlightDelay = 0.5, highlightStagger = 0.3 }) => {
  const containerRef = useGSAPWithFonts((tl, container) => {
    const textEl = container.querySelector('.highlight-text')!;
    const split = SplitText.create(textEl, { type: 'words' });

    // Entrance: words fade in
    tl.from(split.words, {
      y: 20, opacity: 0, duration: 0.5, stagger: 0.05, ease: 'power2.out',
    });

    // Highlight boxes scale in behind target words
    highlights.forEach(({ wordIndex, color }, i) => {
      const word = split.words[wordIndex] as HTMLElement;
      if (!word) return;

      const box = document.createElement('div');
      Object.assign(box.style, {
        position: 'absolute',
        left: `${word.offsetLeft - 4}px`,
        top: `${word.offsetTop - 2}px`,
        width: `${word.offsetWidth + 8}px`,
        height: `${word.offsetHeight + 4}px`,
        background: color,
        borderRadius: '4px',
        zIndex: '-1',
        transformOrigin: 'left center',
        transform: 'scaleX(0)',
      });
      textEl.appendChild(box);

      tl.to(box, {
        scaleX: 1, duration: 0.3, ease: 'power2.out',
      }, highlightDelay + i * highlightStagger);
    });
  });

  return (
    <AbsoluteFill style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
      <div ref={containerRef}>
        <p className="highlight-text" style={{
          fontSize: 64, fontWeight: 'bold', color: '#fff',
          position: 'relative', maxWidth: '70%', lineHeight: 1.2,
        }}>{text}</p>
      </div>
    </AbsoluteFill>
  );
};
```

**Props:** `highlights` is an array of `{ wordIndex, color }` targeting specific words (0-indexed from SplitText).

---

## 2. SVG Animations

### MorphSVG (shape morphing)

```tsx
const containerRef = useGSAPTimeline((tl, container) => {
  tl.to(container.querySelector('#path')!, {
    morphSVG: { shape: '#target-path', type: 'rotational', map: 'size' },
    duration: 1.5, ease: 'power2.inOut',
  });
});
```

| Option | Values |
|--------|--------|
| `type` | `"linear"` (default), `"rotational"` |
| `map` | `"size"`, `"position"`, `"complexity"` |
| `shapeIndex` | Integer for point 

Related in Image & Video