بازگشت به وبلاگ

tailwindcss-motion: کتابخونه‌ی انیمیشنی که پشت هر Fade این سایته

۲ مرداد ۱۴۰۵3 دقیقه مطالعه
tailwindcss-motion: کتابخونه‌ی انیمیشنی که پشت هر Fade این سایته

هر fade، slide و scale توی این سایت روی tailwindcss-motion اجرا می‌شه، نه Framer Motion یا GSAP. این یه پلاگین Tailwind‌ه که @keyframes خام CSS تولید می‌کنه و به‌شکل کلاس‌های utility expose‌شون می‌کنه — motion-opacity-in-0، motion-translate-y-in-[16px]، motion-duration-[500ms]. هیچ موتور انیمیشن جاوااسکریپتی نیست که موقع اجرا فریم‌ها رو حساب کنه؛ خودِ کلاس انیمیشن رو اجرا می‌کنه، دقیقاً مثل هر utility دیگه‌ی Tailwind که فقط یه قانون CSS رو اعمال می‌کنه.

چرا این به‌جای یه کتابخونه‌ی انیمیشن جاوااسکریپتی، این‌جا

  • هزینه‌ی جاوااسکریپت صفر برای انیمیشن‌های ورود و هاور — چون فقط یه کلاس CSS‌ه، یه کامپوننت فقط برای انیمیت‌شدن نیازی به "use client" نداره. motion.div توی Framer Motion یه client boundary می‌خواد و رانتایمشو می‌فرسته؛ این‌جا والد می‌تونه Server Component بمونه، با کلاس‌های انیمیشن که از قبل توی markup‌ای که رندر می‌کنه هست.

  • همون توکن‌های طراحی بقیه‌ی Tailwind — motion-duration-[500ms] دقیقاً مثل هر utility دیگه خونده می‌شه، بدون یه config جدای spring/easing که باید با بقیه‌ی سیستم طراحی sync بمونه.

  • حجم اضافه‌ی خیلی کم به باندل — چون CSS‌ه، نه یه رانتایم جاوااسکریپتی که به هر صفحه‌ای که ازش استفاده می‌کنه فرستاده بشه.

چی رو از دست می‌دی

  • بدون کنترل imperative — نمی‌تونی از یه event handler به یه المان بگی «همین الان دقیقاً به این مقدار انیمیت شو»، فراتر از toggle کردن کلاس‌ها.

  • بدون پشتیبانی drag یا gesture.

  • بدون شبیه‌سازی واقعی فیزیک spring — پریست‌های spring-flavored حسشو تقریب می‌زنن، نه اینکه فریم‌به‌فریم زنده حسابش کنن.

  • تایم‌لاین‌های چندمرحله‌ای و به‌هم‌وابسته (این باید تموم بشه قبل از شروع اون یکی، callback زنجیره‌ای، لغو و برعکس‌کردن وسط انیمیشن) خیلی زود دست‌وپاگیر می‌شن — دقیقاً جایی که یه کتابخونه‌ی جاوااسکریپت‌محور واقعاً حق داره هزینه‌ی سمت کلاینتش رو بگیره.

قانون کلی: این برای کار ساده تا متوسط خوب جواب می‌ده — انیمیشن‌های ورود، میکرو-اینتراکشن‌های هاور، ریویل‌های موقع اسکرول. برای یه برد drag-and-drop واقعی یا یه UI مبتنی بر gesture فیزیکی، سراغ Framer Motion برو و هزینه‌ی سمت کلاینتشو قبول کن؛ اون‌جا واقعاً داره یه مسئله‌ی متفاوت رو حل می‌کنه.

استفاده‌ی پایه — بدون هیچ "use client"

// این یه Server Component ساده‌ست.
export function WelcomeCard() {
  return (
    <div className="motion-opacity-in-0 motion-translate-y-in-[16px] motion-duration-[500ms] motion-ease-spring-smooth rounded-xl border p-6">
      <h3 className="font-semibold">Welcome</h3>
      <p className="text-muted-foreground text-sm">
        This fades and slides in on load, with zero JavaScript.
      </p>
    </div>
  );
}

انیمیشن‌های اسکرول‌محور — بخشی که خودش برات انجام نمی‌ده

tailwindcss-motion فقط حضور کلاس رو می‌شناسه؛ اصلاً نمی‌دونه یه المان کِی وارد viewport شده. برای همینه که این سایت باهاش یه wrapper کوچیک سمت کلاینت جفت کرده که از IntersectionObserver استفاده می‌کنه تا وقتی المان واقعاً دیده شد کلاس‌های انیمیشن رو روشن کنه — همون کامپوننت Reveal که توی هر بخش این سایت استفاده شده.

"use client";
import { useEffect, useRef, useState, type ReactNode } from "react";

export function Reveal({
  children,
  animation,
  threshold = 0.15,
}: {
  children: ReactNode;
  animation: string;
  threshold?: number;
}) {
  const ref = useRef<HTMLDivElement>(null);
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;

    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setVisible(true);
          observer.unobserve(el);
        }
      },
      { threshold }
    );

    observer.observe(el);
    return () => observer.disconnect();
  }, [threshold]);

  return <div className={visible ? animation : "opacity-0"}>{children}</div>;
}
<Reveal animation="motion-opacity-in-0 motion-translate-y-in-[24px] motion-duration-[600ms]">
  <ProjectCard project={project} />
</Reveal>

فقط همین wrapper به "use client" نیاز داره. خودِ ProjectCard، و هرچی که رندر می‌کنه، می‌تونه Server Component بمونه — Reveal فقط یه className رو روی یه div که دورشه toggle می‌کنه، لازم نیست چیزی درباره‌ی محتوای داخلش بدونه.