mcpField
G
gsap-react

gsap-react

github.com/bbeierle12Design & UI9725 installsUpdated 1d ago
Repository

About gsap-react

gsap-react is a skill for applying GSAP animation patterns in React components. It covers the @gsap/react hook, refs, cleanup, timelines, ScrollTrigger, and reusable animation hooks.

Key features

  1. 01useGSAP hook patterns
  2. 02Element and list refs
  3. 03Scoped context and automatic cleanup
  4. 04Event-safe animations
  5. 05Timeline and ScrollTrigger patterns
  6. 06Reusable animation hooks

Use cases

  1. 01Animate components when they mount or update
  2. 02Manage timelines controlled by component state
  3. 03Animate elements in response to events
  4. 04Build reusable fade-in or hover hooks

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/bbeierle12/skill-mcp-claude --skill gsap-react

After installing, type/gsap-reactin Claude Code to confirm it loaded.

Bundled files_meta.json

Details

From the project README

GSAP React Integration

React-specific patterns for GSAP animations.

Quick Start

npm install gsap @gsap/react
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';

function Component() {
  const containerRef = useRef(null);

  useGSAP(() => {
    gsap.to('.box', { x: 200, duration: 1 });
  }, { scope: containerRef });

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

useGSAP Hook

Basic Usage
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';

function AnimatedComponent() {
  const container = useRef(null);

  useGSAP(() => {
    // All GSAP animations here
    gsap.from('.item', {
      opacity: 0,
      y: 50,
      stagger: 0.1
    });
  }, { scope: container }); // Scope limits selector queries

  return (
    <div ref={container}>
      <div className="item">Item 1</div>
      <div className="item">Item 2</div>
      <div className="item">Item 3</div>
    </div>
  );
}
With Dependencies
function AnimatedComponent({ isOpen }) {
  const container = useRef(null);

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

  return (
    <div ref={container}>
      <div className="drawer">Content</div>
    </div>
  );
}
Returning Context
function Component() {
  const container = useRef(null);

  const { context, contextSafe } = useGSAP(() => {
    gsap.to('.box', { x: 200 });
  }, { scope: container });

  // Use contextSafe for event handlers
  const handleClick = contextSafe(() => {
    gsap.to('.box', { rotation: 360 });
  });

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

Ref Patterns

Single Element Ref
function SingleElement() {
  const boxRef = useRef(null);

  useGSAP(() => {
    gsap.to(boxRef.current, {
      x: 200,
      rotation: 360,
      duration: 1
    });
  });

  return <div ref={boxRef}>Box</div>;
}
Multiple Element Refs
function MultipleElements() {
  const itemsRef = useRef([]);

  useGSAP(() => {
    gsap.from(itemsRef.current, {
      opacity: 0,
      y: 30,
      stagger: 0.1
    });
  });

  return (
    <div>
      {[1, 2, 3].map((item, i) => (
        <div
          key={item}
          ref={el => itemsRef.current[i] = el}
        >
          Item {item}
        </div>
      ))}
    </div>
  );
}
Dynamic Refs
function DynamicList({ items }) {
  const itemsRef = useRef(new Map());

  useGSAP(() => {
    gsap.from(Array.from(itemsRef.current.values()), {
      opacity: 0,
      y: 20,
      stagger: 0.05
    });
  }, { dependencies: [items.length] });

  return (
    <div>
      {items.map(item => (
        <div
          key={item.id}
          ref={el => {
            if (el) itemsRef.current.set(item.id, el);
            else itemsRef.current.delete(item.id);
          }}
        >
          {item.name}
        </div>
      ))}
    </div>
  );
}

Context and Cleanup

Automatic Cleanup
// useGSAP automatically cleans up animations on unmount
function Component() {
  useGSAP(() => {
    // This timeline is automatically killed on unmount
    gsap.timeline()
      .to('.a', { x: 100 })
      .to('.b', { x: 100 });
  });
}
Manual Context (Without useGSAP)
import gsap from 'gsap';

function Component() {
  useEffect(() => {
    const ctx = gsap.context(() => {
      gsap.to('.box', { x: 200 });
      gsap.to('.circle', { rotation: 360 });
    });

    return () => ctx.revert(); // Cleanup
  }, []);
}
Scoped Context
function Component() {
  const containerRef = useRef(null);

  useEffect(() => {
    const ctx = gsap.context(() => {
      // Selectors only query within containerRef
      gsap.to('.item', { opacity: 1 });
    }, containerRef);

    return () => ctx.revert();
  }, []);
}

Event Handlers

contextSafe for Events
function InteractiveComponent() {
  const container = useRef(null);

  const { contextSafe } = useGSAP(() => {
    // Initial animation
    gsap.set('.box', { scale: 1 });
  }, { scope: container });

  const handleMouseEnter = contextSafe(() => {
    gsap.to('.box', { scale: 1.1, duration: 0.2 });
  });

  const handleMouseLeave = contextSafe(() => {
    gsap.to('.box', { scale: 1, duration: 0.2 });
  });

  return (
    <div ref={container}>
      <div
        className="box"
        onMouseEnter={handleMouseEnter}
        onMouseLeave={handleMouseLeave}
      >
        Hover me
      </div>
    </div>
  );
}
useCallback Alternative
function Component() {
  const boxRef = useRef(null);
  const tweenRef = useRef(null);

  const animateBox = useCallback(() => {
    tweenRef.current?.kill();
    tweenRef.current = gsap.to(boxRef.current, {
      x: '+=50',
      duration: 0.3
    });
  }, []);

  useEffect(() => {
    return () => tweenRef.current?.kill();
  }, []);

  return <div ref={boxRef} onClick={animateBox}>Click</div>;
}

Timeline Management

Timeline Ref Pattern
function TimelineComponent() {
  const container = useRef(null);
  const tl = useRef(null);

  useGSAP(() => {
    tl.current = gsap.timeline({ paused: true })
      .to('.box', { x: 200 })
      .to('.box', { y: 100 })
      .to('.box', { rotation: 360 });
  }, { scope: container });

  const play = () => tl.current?.play();
  const reverse = () => tl.current?.reverse();
  const restart = () => tl.current?.restart();

  return (
    <div ref={container}>
      <div className="box">Animated</div>
      <button onClick={play}>Play</button>
      <button onClick={reverse}>Reverse</button>
      <button onClick={restart}>Restart</button>
    </div>
  );
}
Controlled Timeline
function ControlledAnimation({ progress }) {
  const container = useRef(null);
  const tl = useRef(null);

  useGSAP(() => {
    tl.current = gsap.timeline({ paused: true })
      .to('.element', { x: 500 })
      .to('.element', { y: 200 });
  }, { scope: container });

  // Update timeline progress when prop changes
  useEffect(() => {
    if (tl.current) {
      tl.current.progress(progress);
    }
  }, [progress]);

  return (
    <div ref={container}>
      <div className="element">Controlled</div>
    </div>
  );
}

ScrollTrigger in React

Basic ScrollTrigger
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';

gsap.registerPlugin(ScrollTrigger);

function ScrollComponent() {
  const container = useRef(null);

  useGSAP(() => {
    gsap.from('.section', {
      opacity: 0,
      y: 100,
      scrollTrigger: {
        trigger: '.section',
        start: 'top 80%',
        toggleActions: 'play none none none'
      }
    });
  }, { scope: container });

  return (
    <div ref={container}>
      <div className="section">Scroll to reveal</div>
    </div>
  );
}
ScrollTrigger Cleanup
function ScrollComponent() {
  const container = useRef(null);

  useGSAP(() => {
    const triggers = [];

    gsap.utils.toArray('.item').forEach(item => {
      const trigger = ScrollTrigger.create({
        trigger: item,
        start: 'top 80%',
        onEnter: () => gsap.to(item, { opacity: 1 })
      });
      triggers.push(trigger);
    });

    // Return cleanup function
    return () => triggers.forEach(t => t.kill());
  }, { scope: container });
}

Custom Hooks

useAnimation Hook
function useAnimation(animation, deps = []) {
  const elementRef = useRef(null);
  const tweenRef = useRef(null);

  useGSAP(() => {
    if (elementRef.current) {
      tweenRef.current = animation(elementRef.current);
    }
    return () => tweenRef.current?.kill();
  }, { dependencies: deps });

  return elementRef;
}

// Usage
function Component() {
  const boxRef = useAnimation((el) =>
    gsap.from(el, { opacity: 0, y: 50, duration: 0.5 })
  );

  return <div ref={boxRef}>Animated</div>;
}
useFadeIn Hook
function useFadeIn(options = {}) {
  const { duration = 0.5, delay = 0, y = 30 } = options;
  const ref = useRef(null);

  useGSAP(() => {
    gsap.from(ref.current, {
      opacity: 0,
      y,
      duration,
      delay,
      ease: 'power2.out'
    });
  });

  return ref;
}

// Usage
function Card() {
  const cardRef = useFadeIn({ delay: 0.2 });
  return <div ref={cardRef}>Card content</div>;
}
useHoverAnimation Hook
function useHoverAnimation(enterAnimation, leaveAnimation) {
  const ref = useRef(null);
  const { contextSafe } = useGSAP({ scope: ref });

  const onEnter = contextSafe(() => enterAnimation(ref.current));
  const onLeave = contextSafe(() => leaveAnimation(ref.current));

  return { ref, onMouseEnter: onEnter, onMouseLeave: onLeave };
}

// Usage
function Button() {
  const hoverProps = useHoverAnimation(
    (el) => gsap.to(el, { scale: 1.05, duration: 0.2 }),
    (el) => gsap.to(el, { scale: 1, duration: 0.2 })
  );

  return <button {...hoverProps}>Hover me</button>;
}

Temporal Collapse Patterns

Animated Countdown Digit
function CountdownDigit({ value, label }) {
  const digitRef = useRef(null);
  const prevValue = useRef(value);

  useGSAP(() => {
    if (prevValue.current !== value) {
      gsap.timeline()
        .to(digitRef.current, {
          rotationX: -90,
          opacity: 0,
          duration: 0.25,
          ease: 'power2.in'
        })
        .call(() => {
          digitRef.current.textContent = value;
          prevValue.current = value;
        })
        .fromTo(digitRef.current,
          { rotationX: 90, opacity: 0 },
          { rotationX: 0, opacity: 1, duration: 0.25, ease: 'power2.out' }
        );
    }
  }, { dependencies: [value] });

  return (
    <div className="digit-container">
      <span ref={digitRef} className="digit">{value}</span>
      <span className="label">{label}</span>
    </div>
  );
}
Cosmic Pulse Effect
function CosmicPulse({ children, color = '#00F5FF' }) {
  const containerRef = useRef(null);

  useGSAP(() => {
    gsap.to(containerRef.current, {
      boxShadow: `0 0 30px ${color}, 0 0 60px ${color}`,
      duration: 1,
      repeat: -1,
      yoyo: true,
      ease: 'sine.inOut'
    });
  }, { scope: containerRef });

  return <div ref={containerRef}>{children}</div>;
}

Performance Tips

// 1. Use will-change for heavy animations
gsap.set('.animated', { willChange: 'transform' });

// 2. Batch similar animations
useGSAP(() => {
  gsap.to('.item', { opacity: 1, stagger: 0.1 }); // Single tween
  // Not: items.forEach(item => gsap.to(item, ...)) // Multiple tweens
});

// 3. Use refs over selectors for frequently animated elements
const boxRef = useRef(null);
gsap.to(boxRef.current, { x: 100 }); // Faster

// 4. Kill animations on rapid state changes
const tweenRef = useRef(null);
useEffect(() => {
  tweenRef.current?.kill();
  tweenRef.current = gsap.to(...);
}, [dependency]);

Reference

  • See gsap-fundamentals for animation basics
  • See gsap-sequencing for timeline composition
  • See gsap-scrolltrigger for scroll-based animations

Comments

Markdown supported