مقالات › React

آموزش GSAP و ScrollTrigger در React؛ انیمیشن‌های اسکرول حرفه‌ای قدم‌به‌قدم

انیمیشن‌های روان و حرفه‌ای در React با GSAP بسازید: Tween و Timeline، پاک‌سازی با useGSAP، ظاهر شدن با ScrollTrigger، Scrub و Pin، اسکرول نرم با Lenis، SplitText و نکات کارایی.

حرکت خوب رابط کاربری را زنده می‌کند: محتوایی که هنگام اسکرول بالا می‌آید، تیترهایی که کلمه‌به‌کلمه ظاهر می‌شوند و بخش‌هایی که ثابت می‌مانند تا یک داستان روایت شود. GSAP (GreenSock Animation Platform) کتابخانه‌ی استاندارد صنعت برای این کار است — سریع، دقیق و مستقل از فریم‌ورک — و با پکیج @gsap/react به‌شکل تمیزی با React یکپارچه می‌شود. انیمیشن‌های همین سایت دقیقاً همین‌طور ساخته شده‌اند.

نصب

npm install gsap @gsap/react

از GSAP 3.13 همه‌ی پلاگین‌ها — از جمله ScrollTrigger و SplitText — رایگان هستند.

import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { useGSAP } from "@gsap/react";

gsap.registerPlugin(ScrollTrigger, useGSAP);

Tween؛ آجر اصلی

یک Tween ویژگی‌ها را از یک حالت به حالت دیگر انیمیت می‌کند:

gsap.to(".card", { y: -20, opacity: 1, duration: 0.8, ease: "expo.out" });
gsap.from(".title", { y: 60, opacity: 0, duration: 1 });          // از این مقادیر تا مقدار فعلی
gsap.fromTo(".bar", { scaleX: 0 }, { scaleX: 1, duration: 1.2 }); // شروع و پایان صریح

گزینه‌های پرکاربرد: duration، delay، ease، stagger (فاصله بین چند هدف)، repeat، yoyo و onComplete.

gsap.from(".skill", { y: 40, opacity: 0, stagger: 0.08, ease: "power3.out" });

Timeline؛ طراحی رقص حرکات

یک Timeline Tweenها را با زمان‌بندی دقیق پشت سر هم می‌چیند:

const tl = gsap.timeline({ defaults: { ease: "expo.out", duration: 1 } });

tl.from(".hero__badge", { y: 24, opacity: 0 })
  .from(".hero__title span", { yPercent: 110, stagger: 0.12 }, "-=0.6") // هم‌پوشانی
  .from(".hero__actions > *", { y: 20, opacity: 0, stagger: 0.1 }, "<0.2") // نسبت به شروع قبلی
  .from(".hero__visual", { scale: 0.85, opacity: 0, duration: 1.6 }, 0.3); // موقعیت مطلق

پارامترهای موقعیت ("-=0.6"، "<"، 0.3) اجازه می‌دهند انیمیشن‌ها را بدون درگیری با Delay هم‌پوشانی و تراز کنید. Timeline را می‌توان به‌صورت یک واحد متوقف، معکوس یا با اسکرول کنترل کرد.

روش React: useGSAP

در React انیمیشن‌ها باید هنگام Unmount یا رندر مجدد پاک‌سازی شوند؛ وگرنه Tweenهای تکراری و ScrollTriggerهای نشت‌کرده خواهید داشت (مخصوصاً در StrictMode). useGSAP این کار را خودکار انجام می‌دهد:

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

  useGSAP(
    () => {
      gsap.from(".hero__title", { y: 80, opacity: 0, duration: 1.2, ease: "expo.out" });
    },
    { scope: container } // Selectorها به همین المان محدود می‌شوند
  );

  return (
    <section ref={container}>
      <h1 className="hero__title">Hello</h1>
    </section>
  );
}
  • scope متن Selectorهایی مثل ".hero__title" را به همان کامپوننت محدود می‌کند تا کامپوننت‌های دیگر تحت تأثیر قرار نگیرند.
  • هر چیزی که داخل آن ساخته شود هنگام Unmount برگردانده می‌شود.
  • برای انیمیشن‌هایی که با رویداد اجرا می‌شوند، Handlerها را با contextSafe بپیچید.

اگر از این هوک استفاده نمی‌کنید، gsap.context() داخل useLayoutEffect همراه با return () => ctx.revert() همان کار را انجام می‌دهد.

ScrollTrigger؛ انیمیشن با اسکرول

ظاهر شدن وقتی المان وارد دید می‌شود

useGSAP(() => {
  gsap.utils.toArray(".reveal").forEach((el) => {
    gsap.from(el, {
      y: 50,
      opacity: 0,
      duration: 1,
      ease: "expo.out",
      scrollTrigger: {
        trigger: el,
        start: "top 85%",   // وقتی بالای المان به ۸۵ درصد ارتفاع صفحه برسد
        once: true,
      },
    });
  });
}, { scope: container });

برای المان‌های زیاد، ScrollTrigger.batch المان‌هایی را که با هم وارد دید می‌شوند گروه‌بندی و پلکانی نمایش می‌دهد:

ScrollTrigger.batch(".card", {
  start: "top 88%",
  once: true,
  onEnter: (batch) => gsap.from(batch, { y: 40, opacity: 0, stagger: 0.08, ease: "expo.out" }),
});

Scrub؛ گره زدن پیشرفت انیمیشن به اسکرول

gsap.to(".hero__content", {
  yPercent: -10,
  opacity: 0.2,
  ease: "none",
  scrollTrigger: {
    trigger: ".hero",
    start: "top top",
    end: "bottom top",
    scrub: true,       // یا یک عدد (مثلاً 0.5) برای نرمی بیشتر
  },
});

Pin کردن

const tl = gsap.timeline({
  scrollTrigger: {
    trigger: ".story",
    start: "top top",
    end: "+=2000",
    scrub: 1,
    pin: true,           // بخش ثابت می‌ماند تا Timeline اجرا شود
  },
});

tl.to(".story__step--1", { opacity: 0 })
  .from(".story__step--2", { opacity: 0, y: 40 })
  .from(".story__step--3", { opacity: 0, y: 40 });

در حین توسعه markers: true را اضافه کنید تا موقعیت شروع و پایان را ببینید.

اسکرول نرم با Lenis

کتابخانه‌های اسکرول نرم، انیمیشن‌های وابسته به اسکرول را حرفه‌ای‌تر می‌کنند. Lenis سبک است و با ScrollTrigger کار می‌کند:

import Lenis from "lenis";

useEffect(() => {
  const lenis = new Lenis({ duration: 1.15, smoothWheel: true });

  lenis.on("scroll", ScrollTrigger.update);           // هماهنگ نگه داشتن Triggerها
  const tick = (time) => lenis.raf(time * 1000);
  gsap.ticker.add(tick);                               // یک حلقه‌ی انیمیشن مشترک
  gsap.ticker.lagSmoothing(0);

  return () => {
    gsap.ticker.remove(tick);
    lenis.destroy();
  };
}, []);

یادتان باشد وقتی مودال باز است Lenis را متوقف کنید (lenis.stop()) و برای کاربرانی که prefers-reduced-motion دارند آن را فعال نکنید.

انیمیشن متن با SplitText

import { SplitText } from "gsap/SplitText";
gsap.registerPlugin(SplitText);

const split = SplitText.create(".headline", { type: "words, chars" });
gsap.from(split.chars, { yPercent: 100, opacity: 0, stagger: 0.02, ease: "expo.out" });

نکته: متن فارسی و عربی را به کاراکتر تقسیم نکنید؛ اتصال حروف را خراب می‌کند. با تقسیم به کلمه هم احتیاط کنید: وقتی هر کلمه inline-block شود، عبارت‌های انگلیسی داخل متن راست‌چین ممکن است برعکس نمایش داده شوند. متن RTL را بر اساس خط تقسیم کنید.

Refresh بعد از تغییر چیدمان

ScrollTrigger موقعیت‌ها را هنگام بارگذاری اندازه می‌گیرد. وقتی بعداً ارتفاع محتوا تغییر می‌کند — تصاویر بارگذاری می‌شوند، داده می‌رسد یا آکاردئون باز می‌شود — این را صدا بزنید:

ScrollTrigger.refresh();

در SPAها بعد از تغییر مسیر، وقتی صفحه‌ی جدید رندر شد، هم Refresh کنید.

کارایی و دسترس‌پذیری

  • transform و opacity را انیمیت کنید (x، y، scale، rotation، autoAlpha). از انیمیت کردن width، top، left یا box-shadow روی نواحی بزرگ پرهیز کنید.
  • از will-change کم استفاده کنید؛ GSAP بیشتر بهینه‌سازی‌ها را خودش انجام می‌دهد.
  • مدت‌ها را هدفمند نگه دارید (۰٫۴ تا ۱٫۲ ثانیه)؛ حرکت باید توجه را هدایت کند، نه کاربر را معطل.
  • به کاهش حرکت احترام بگذارید:
const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: no-preference)", () => {
  // انیمیشن‌های پرحرکت فقط اینجا ثبت شوند
});
  • مطمئن شوید محتوا بدون جاوااسکریپت یا قبل از اجرای انیمیشن قابل دیدن است (متن حیاتی را با CSSی که فقط JS می‌تواند برگرداند مخفی نکنید).

جمع‌بندی

GSAP با Tween و Timeline کنترل دقیق می‌دهد؛ useGSAP آن را با پاک‌سازی و محدوده‌بندی خودکار در React امن می‌کند؛ ScrollTrigger انیمیشن را با ظاهر شدن، Scrub و Pin به اسکرول وصل می‌کند؛ Lenis اسکرول نرم را روی همان Ticker اضافه می‌کند و SplitText تیترها را انیمیت می‌کند. Transform و Opacity را انیمیت کنید، بعد از تغییر چیدمان Refresh کنید و به ترجیح کاهش حرکت احترام بگذارید تا سایت شما بدون قربانی کردن کارایی، حرفه‌ای به نظر برسد.

پرسش‌های متداول

GSAP یا Framer Motion؟

Framer Motion (Motion) برای انیمیشن‌های اعلانی کامپوننت و چیدمان عالی است. GSAP در Timelineهای پیچیده، توالی‌های وابسته به اسکرول و کنترل دقیق می‌درخشد. خیلی از پروژه‌ها یکی و برخی هر دو را برای نیازهای مختلف استفاده می‌کنند.

چرا ScrollTriggerهای من در موقعیت اشتباه اجرا می‌شوند؟

معمولاً چون چیدمان بعد از ساخته شدنشان تغییر کرده (تصاویر، فونت‌ها، داده). بعد از این تغییرات ScrollTrigger.refresh() را صدا بزنید.

آیا GSAP با رندر سمت سرور کار می‌کند؟

بله؛ انیمیشن‌ها را فقط در سمت کلاینت (داخل Effect یا useGSAP) اجرا کنید، هرگز هنگام رندر.