← Back to blog

Optimizing React with Tailwind CSS and GSAP for Stunning User Interfaces

February 15, 2026·12 min read
ReactTailwind CSSGSAPUI Design

Creating performant, animated user interfaces in React requires the right tooling. Tailwind CSS handles styling with utility classes, and GSAP provides a battle-tested animation engine. This post covers how to set them up together in a Next.js project, common pitfalls with GSAP in React, and practical patterns for accessible animations.

Why Choose Tailwind CSS and GSAP with React?

Tailwind CSS and GSAP solve different problems that complement each other well. Tailwind gives you a utility-first styling system that keeps styles co-located with markup and produces minimal CSS in production. GSAP is a JavaScript animation library that handles complex timelines, scroll-triggered animations, and hardware-accelerated transforms — things CSS animations struggle with at scale.

Benefits of Tailwind CSS

  • Utility-First Design: Tailwind CSS provides utility classes for styling directly in your JSX, eliminating context-switching between CSS files and components.
  • Responsive Design: Built-in breakpoint prefixes (sm:, md:, lg:) make responsive layouts straightforward.
  • Customization: The configuration system lets you define design tokens (colors, spacing, fonts) that enforce consistency across your project.

Advantages of GSAP

  • Performance: GSAP uses requestAnimationFrame and optimizes property batching to avoid layout thrashing. It consistently outperforms CSS animations for complex sequences.
  • Versatility: Timelines, scroll triggers, morphing SVGs, and physics-based animations are all built in or available as plugins.
  • Browser Consistency: GSAP normalizes animation behavior across browsers, handling edge cases that CSS transitions leave to the developer.

Setting Up Your Project

Here's how to set up a new Next.js project with Tailwind CSS v4 and GSAP:

npx create-next-app@latest my-project
cd my-project
npm install gsap

Configuring Tailwind CSS

Tailwind CSS v4 ships as a Vite plugin or PostCSS plugin and uses CSS-based configuration instead of the old tailwind.config.js. In your global CSS file (e.g., app/globals.css), import Tailwind:

@import "tailwindcss";

To customize your theme, use @theme directly in CSS:

@import "tailwindcss";
 
@theme {
  --color-primary: #2563eb;
  --color-primary-dark: #1d4ed8;
}

This replaces the old JavaScript config file from Tailwind v3. If you're on Tailwind v3, you'd use tailwind.config.js with content paths instead — but v4 handles content detection automatically.

Integrating GSAP

Import GSAP directly — there's no need for a React wrapper library:

import { gsap } from 'gsap';

For scroll-triggered animations, register the plugin once at the top level:

import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
 
gsap.registerPlugin(ScrollTrigger);

Building a Responsive and Animated Interface

Let's build a landing page that combines Tailwind for layout and GSAP for entrance animations.

Creating the Layout with Tailwind CSS

import React from 'react';
 
const LandingPage: React.FC = () => {
  return (
    <div className="min-h-screen bg-gray-100 flex flex-col items-center justify-center">
      <h1 className="text-4xl font-bold text-center text-blue-600 mb-6">
        Welcome to My Stunning React App
      </h1>
      <p className="text-lg text-center text-gray-700 mb-8">
        Built with React, Tailwind CSS, and GSAP for amazing user experiences
      </p>
      <button className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition">
        Get Started
      </button>
    </div>
  );
};
 
export default LandingPage;

Adding Animations with GSAP

When using GSAP in React, there are two critical things to get right: cleanup and flash prevention.

GSAP animations must be cleaned up when a component unmounts, otherwise you'll get memory leaks and stale animations. In React 18+ with StrictMode, effects run twice in development — without cleanup, you'll see doubled or broken animations. Use gsap.context() to scope animations and revert them on unmount.

To prevent a flash of unstyled content (elements appearing at full opacity before GSAP snaps them to opacity: 0), use useLayoutEffect instead of useEffect. This runs synchronously before the browser paints, so users never see the pre-animation state.

import React, { useLayoutEffect, useRef } from 'react';
import { gsap } from 'gsap';
 
const LandingPage: React.FC = () => {
  const containerRef = useRef<HTMLDivElement>(null);
  const titleRef = useRef<HTMLHeadingElement>(null);
  const buttonRef = useRef<HTMLButtonElement>(null);
 
  useLayoutEffect(() => {
    const ctx = gsap.context(() => {
      gsap.fromTo(
        titleRef.current,
        { opacity: 0, y: -50 },
        { opacity: 1, y: 0, duration: 1, ease: 'power3.out' }
      );
      gsap.fromTo(
        buttonRef.current,
        { opacity: 0, y: 50 },
        { opacity: 1, y: 0, duration: 1, delay: 0.5, ease: 'power3.out' }
      );
    }, containerRef);
 
    return () => ctx.revert();
  }, []);
 
  return (
    <div ref={containerRef} className="min-h-screen bg-gray-100 flex flex-col items-center justify-center">
      <h1 ref={titleRef} className="text-4xl font-bold text-center text-blue-600 mb-6 opacity-0">
        Welcome to My Stunning React App
      </h1>
      <p className="text-lg text-center text-gray-700 mb-8">
        Built with React, Tailwind CSS, and GSAP for amazing user experiences
      </p>
      <button ref={buttonRef} className="bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition opacity-0">
        Get Started
      </button>
    </div>
  );
};
 
export default LandingPage;

Key differences from a naive implementation:

  • gsap.context() scopes all animations to the container and provides a .revert() method for cleanup
  • useLayoutEffect prevents the flash of content before animations begin
  • gsap.fromTo instead of gsap.from explicitly defines both start and end states, avoiding visual glitches
  • opacity-0 on animated elements as a CSS baseline, so even without JavaScript, the initial state is defined

Respecting User Preferences with prefers-reduced-motion

Not all users want animations. Some have vestibular disorders, and others simply prefer less motion. Always check the prefers-reduced-motion media query:

useLayoutEffect(() => {
  const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
 
  const ctx = gsap.context(() => {
    if (prefersReducedMotion) {
      // Skip animation, just show elements immediately
      gsap.set([titleRef.current, buttonRef.current], { opacity: 1, y: 0 });
      return;
    }
 
    gsap.fromTo(
      titleRef.current,
      { opacity: 0, y: -50 },
      { opacity: 1, y: 0, duration: 1, ease: 'power3.out' }
    );
    gsap.fromTo(
      buttonRef.current,
      { opacity: 0, y: 50 },
      { opacity: 1, y: 0, duration: 1, delay: 0.5, ease: 'power3.out' }
    );
  }, containerRef);
 
  return () => ctx.revert();
}, []);

Best Practices for Optimizing Performance

Animate Transforms and Opacity Only

GSAP can animate any CSS property, but animating width, height, top, left, or margin triggers layout recalculations (reflows). Stick to transform (x, y, scale, rotation) and opacity — these run on the compositor thread and don't trigger reflows.

// Good: compositor-only properties
gsap.to(element, { x: 100, opacity: 0.5, scale: 1.2 });
 
// Avoid: triggers layout recalculation
gsap.to(element, { width: '200px', marginLeft: '20px' });

Use GSAP Timelines for Sequenced Animations

Instead of managing multiple tweens with delay offsets, use gsap.timeline(). Timelines make sequences easier to maintain and allow you to control playback (pause, reverse, seek):

useLayoutEffect(() => {
  const ctx = gsap.context(() => {
    const tl = gsap.timeline({ defaults: { ease: 'power3.out' } });
 
    tl.fromTo(titleRef.current, { opacity: 0, y: -50 }, { opacity: 1, y: 0, duration: 1 })
      .fromTo(buttonRef.current, { opacity: 0, y: 50 }, { opacity: 1, y: 0, duration: 1 }, '-=0.5');
  }, containerRef);
 
  return () => ctx.revert();
}, []);

The '-=0.5' position parameter means the button animation starts 0.5s before the title animation ends, creating an overlap without hardcoding delay values.

Tailwind CSS Production Optimization

Tailwind v4 automatically tree-shakes unused styles — no configuration needed. If you're on v3, the content array in tailwind.config.js serves this purpose. Either way, the production build only includes the utility classes actually used in your markup.

Deploying Your Application

Once your application is ready, deploying with Vercel is the most straightforward option for Next.js projects. Vercel's integration handles build optimization, edge caching, and serverless functions out of the box. Azure Static Web Apps or AWS Amplify are solid alternatives if your infrastructure is already on those platforms.

Conclusion

Tailwind CSS and GSAP are a strong pairing for React UIs — Tailwind handles the styling system while GSAP handles complex animations that CSS can't easily express. The key takeaways: always clean up GSAP animations with gsap.context(), use useLayoutEffect to prevent content flash, respect prefers-reduced-motion, and stick to compositor-friendly properties for smooth 60fps animations.

For more insights into my projects or what I offer, feel free to explore my work and my services.

Related Posts