Crafting Dynamic User Interfaces with React 19.2 and GSAP 3.12
Creating dynamic user interfaces that captivate users is a crucial aspect of modern web development. With React 19.2 and GSAP 3.12, we can achieve seamless animations while maintaining top-notch performance. As an experienced frontend engineer specializing in React, Next.js, and TypeScript, I often leverage these tools to enhance user experience. In this post, I'll walk you through practical techniques for integrating GSAP animations into React applications, covering mount animations, interaction-driven tweens, scroll-triggered effects, and the performance considerations that tie them all together.
Why Use GSAP with React?
GSAP, short for GreenSock Animation Platform, is a powerful JavaScript library that excels at creating high-performance animations. Unlike CSS animations, GSAP gives you imperative control over every aspect of a tween: easing curves, stagger, callbacks, pausing, reversing, and timeline sequencing, without fighting specificity or cascade rules.
The main challenge historically has been integrating GSAP cleanly with React's rendering model. React's Strict Mode intentionally double-invokes effects in development to surface side-effect bugs, which causes naive GSAP setups to fire animations twice. The @gsap/react package solves this with the useGSAP hook.
Key Benefits
- Performance: GSAP batches DOM reads/writes and uses
requestAnimationFrameinternally, keeping animations off the main thread as much as possible. - Flexibility: Timelines, stagger, morphSVG, ScrollTrigger, Flip; a full animation toolkit rather than a single utility.
- React integration:
useGSAPhandles context creation and cleanup automatically, making it a true drop-in foruseEffect.
Setting Up Your Environment
npm install react@19 gsap @gsap/reactIf you're on TypeScript, GSAP ships its own types, so no @types/gsap is needed. Ensure your tsconfig.json has at minimum:
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true
}
}Register plugins once at the module level, before any component code runs:
import { gsap } from 'gsap';
import { useGSAP } from '@gsap/react';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(useGSAP, ScrollTrigger);Calling registerPlugin at the top of a shared module (e.g. lib/gsap.ts) ensures it only runs once regardless of how many components import it.
Mount Animations
The simplest pattern is animating elements when a component mounts. useGSAP wraps gsap.context() internally, so all tweens created inside it are automatically reverted on unmount with no manual cleanup required.
import React, { useRef } from 'react';
import { gsap } from 'gsap';
import { useGSAP } from '@gsap/react';
const FadeInComponent: React.FC = () => {
const containerRef = useRef<HTMLDivElement | null>(null);
useGSAP(() => {
gsap.from(containerRef.current, { opacity: 0, y: 24, duration: 0.6, ease: 'power2.out' });
}, { scope: containerRef });
return (
<div ref={containerRef}>
Welcome to the dynamic world of animations!
</div>
);
};
export default FadeInComponent;A few things worth noting here:
gsap.fromanimates from the given values to the element's current state, so there's no need to setopacity: 0inline.- Adding a
yoffset alongsideopacitygives the entry a natural feel, as purely opacity fades tend to look flat. scope: containerRefscopes any selector strings used inside the callback to that subtree, which becomes important once you have multiple instances of the same component on a page.
Timeline Animations
Timelines are where GSAP really shines over CSS. They let you sequence tweens, overlap them with position parameters, and control the entire sequence as a single unit: pause it, reverse it, or scrub it with scroll.
import React, { useRef } from 'react';
import { gsap } from 'gsap';
import { useGSAP } from '@gsap/react';
const CardReveal: React.FC = () => {
const containerRef = useRef<HTMLDivElement | null>(null);
useGSAP(() => {
const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
tl.from('.card', { opacity: 0, y: 40, duration: 0.5 })
.from('.card-title', { opacity: 0, x: -20, duration: 0.4 }, '-=0.2')
.from('.card-body', { opacity: 0, duration: 0.4 }, '-=0.1');
}, { scope: containerRef });
return (
<div ref={containerRef}>
<div className="card">
<h2 className="card-title">Hello</h2>
<p className="card-body">This reveals with a choreographed sequence.</p>
</div>
</div>
);
};
export default CardReveal;The '-=0.2' position parameter starts the next tween 0.2 seconds before the previous one ends, creating overlapping animations that feel much more fluid than strict sequencing. defaults on the timeline lets you set shared properties (like ease) once rather than repeating them on every tween.
Interaction-Triggered Animations
Event handlers and callbacks run after the initial useGSAP execution, outside GSAP's context. To ensure those animations are still tracked and cleaned up, wrap them with contextSafe, which is returned from the useGSAP hook.
import React, { useRef } from 'react';
import { gsap } from 'gsap';
import { useGSAP } from '@gsap/react';
const HoverButton: React.FC = () => {
const containerRef = useRef<HTMLDivElement | null>(null);
const { contextSafe } = useGSAP({ scope: containerRef });
const onEnter = contextSafe(() => {
gsap.to('.btn', { scale: 1.05, duration: 0.2, ease: 'power1.out' });
});
const onLeave = contextSafe(() => {
gsap.to('.btn', { scale: 1, duration: 0.2, ease: 'power1.in' });
});
return (
<div ref={containerRef}>
<button
className="btn"
onMouseEnter={onEnter}
onMouseLeave={onLeave}
>
Hover me
</button>
</div>
);
};
export default HoverButton;Without contextSafe, the tweens created inside onEnter and onLeave would not be associated with the component's GSAP context. They would persist after unmount, potentially causing memory leaks or animating detached DOM nodes.
Scroll-Triggered Animations
ScrollTrigger is a GSAP plugin that ties animation progress to scroll position. Because it registers DOM observers and scroll listeners, cleanup is especially important. useGSAP handles this automatically when ScrollTrigger instances are created inside the hook.
import React, { useRef } from 'react';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import { useGSAP } from '@gsap/react';
gsap.registerPlugin(useGSAP, ScrollTrigger);
const ScrollReveal: React.FC = () => {
const containerRef = useRef<HTMLDivElement | null>(null);
useGSAP(() => {
gsap.from('.reveal-item', {
opacity: 0,
y: 50,
duration: 0.7,
ease: 'power2.out',
stagger: 0.15,
scrollTrigger: {
trigger: containerRef.current,
start: 'top 80%',
end: 'bottom 20%',
toggleActions: 'play none none reverse',
},
});
}, { scope: containerRef });
return (
<div ref={containerRef}>
{['First', 'Second', 'Third'].map((label) => (
<div key={label} className="reveal-item" style={{ padding: '2rem 0' }}>
{label} item
</div>
))}
</div>
);
};
export default ScrollReveal;stagger: 0.15 offsets each .reveal-item by 150ms, so the list reads top-to-bottom rather than all elements appearing simultaneously. toggleActions controls what happens at the four trigger events (enter, leave, enter-back, leave-back). 'play none none reverse' means the animation plays on scroll-into-view and reverses when scrolling back up.
Performance Considerations
Animate composited properties
Stick to transform (including x, y, rotation, scale) and opacity. These are the only CSS properties browsers can animate entirely on the GPU compositor thread, without triggering layout or paint. Animating width, height, top, left, or margin forces reflow on every frame.
Avoid creating tweens inside render
GSAP tweens should always be created inside useGSAP or contextSafe callbacks, never directly in the render body. Creating a tween during render means a new animation starts on every re-render, not just on mount.
// Bad: new tween created on every render
const Component = () => {
gsap.to('.el', { x: 100 }); // runs every render
return <div className="el" />;
};
// Good: runs once on mount
const Component = () => {
const ref = useRef(null);
useGSAP(() => {
gsap.to('.el', { x: 100 });
}, { scope: ref });
return <div ref={ref} className="el" />;
};Use will-change sparingly
will-change: transform can hint to the browser to promote an element to its own compositor layer ahead of time. But overusing it consumes significant GPU memory, so only apply it to elements you know will animate, and remove it after the animation completes if possible.
Pause off-screen animations
For long-running or looping animations, use ScrollTrigger's toggleActions or scrub to pause them when not in the viewport. A looping tween running in a tab the user has scrolled past is pure wasted computation.
Conclusion
Pairing GSAP with React gives you a capable, production-ready animation stack. The useGSAP hook removes the friction that used to make GSAP + React awkward: cleanup, Strict Mode double-invocation, and context scoping are all handled for you. Start with mount animations to get comfortable with the API, then layer in timelines for choreographed sequences, contextSafe for interaction handlers, and ScrollTrigger for scroll-driven effects.
For more on how I approach frontend development, check out my projects or explore what I offer.
Related Posts
Leveraging the View Transitions API in React 19 for Smooth User Experiences
Explore how to implement the View Transitions API in React 19 to create seamless and visually appealing user experiences on your web applications.
Optimizing React with Tailwind CSS and GSAP for Stunning User Interfaces
Discover how to use React with Tailwind CSS and GSAP to create visually compelling and high-performance web applications.
Testing React Applications: A Comprehensive Guide for Robust Code
Learn how to effectively test React applications using TypeScript, focusing on unit, integration, and end-to-end testing for reliable software.