پرتوپرتو

حرکت و انیمیشن

مدت، easing و انیمیشن‌های آماده پرتو — و اینکه کدام‌شان از توکن می‌خوانند و کدام‌شان نه

اصل

حرکت در یک محصول گزارش‌محور کار تزئین نمی‌کند؛ تغییر را توضیح می‌دهد. کاربری که فیلتر را عوض می‌کند باید ببیند جدول عوض شد، نه اینکه ناگهان محتوای دیگری روی صفحه باشد. پس هر انیمیشن باید به یک سؤال جواب بدهد: «چه چیزی از کجا به کجا رفت؟» اگر جوابی ندارد، حذفش کنید.

سه مدت و سه easing، کل واژگان حرکت این سیستم است. بیشتر از این، ناهماهنگی می‌سازد.


نمونه بصری

easing را انتخاب کنید، بعد روی هر نوار بزنید:

پیش‌فرض — ورود و خروج متعادل

cubic-bezier(0.4, 0, 0.2, 1)

--duration-fast 0ms

بازخورد فوری: hover، focus، تغییر رنگ

--duration-normal 0ms

ورود و خروج المان: dropdown، tooltip، sheet

--duration-slow 0ms

جابه‌جایی بزرگ‌تر: drawer، تغییر چیدمان

مقادیر از globals.css تولید می‌شوند. اگر prefers-reduced-motion: reduce روی سیستم شما فعال باشد، هر سه مدت صفر است و نوارها بدون حرکت جابه‌جا می‌شوند — که رفتار درست است، نه خرابی.


قوانین اجباری

مدت و easing از توکن بیایند

// ✅ درست — کلاس‌های duration-* روی توکن‌های سیستم سوار هستند
<div className="transition-colors duration-fast"></div>
<div className="transition-transform duration-normal"></div>

// ❌ غلط — عدد دستی، که با تغییر مقیاس سیستم عقب می‌ماند
<div className="transition-colors duration-[137ms]"></div>
<div style={{ transitionDuration: '150ms' }}></div>
توکنمقدارکِی
--duration-fast150msبازخورد فوری: hover، focus، تغییر رنگ
--duration-normal200msورود و خروج المان: dropdown، tooltip، sheet
--duration-slow300msجابه‌جایی بزرگ‌تر: drawer تمام‌قد، تغییر چیدمان
--easing-standardcubic-bezier(0.4, 0, 0.2, 1)پیش‌فرض؛ ورود و خروج متعادل
--easing-deceleratecubic-bezier(0, 0, 0.2, 1)ورود — سریع شروع می‌کند، آرام می‌ایستد
--easing-acceleratecubic-bezier(0.4, 0, 1, 1)خروج — آرام شروع می‌کند، سریع می‌رود

توکن‌های مدت، انیمیشن‌های آماده را کنترل نمی‌کنند

کلاس‌های duration-fast/normal/slow در tailwind.config.ts به همین توکن‌ها نگاشت شده‌اند، پس مدتِ transitionها واقعاً از توکن می‌آید. اما انیمیشن‌های آمادهٔ سیستم (animate-fade-in، animate-slide-up و بقیه) زمانشان را درجا نوشته‌اند — مثلاً 0.2s — و از توکن نمی‌خوانند. یعنی اگر مقیاس مدت را عوض کنید، transitionها عوض می‌شوند و آن انیمیشن‌ها نه. اگر به مدتِ توکن‌محور نیاز دارید، به‌جای animate-* یک transition بنویسید.

ترجیح کاربر، شرط نیست — پیش‌فرض است

/* پرتو این را در globals.css خودش دارد؛ لازم نیست تکرارش کنید */
@media (prefers-reduced-motion: reduce) {
  :root {
    --duration-fast: 0ms;
    --duration-normal: 0ms;
    --duration-slow: 0ms;
  }
  *,
  ::before,
  ::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }
}

دو لایه است و عمداً: لایهٔ اول توکن‌ها را صفر می‌کند، لایهٔ دوم هر انیمیشنی را می‌گیرد که توکن نمی‌خواند — از جمله همان animate-*های بالا. پس حتی انیمیشن سفارشی شما هم زیر این ترجیح می‌ایستد.

برای حرکتی که با جاوااسکریپت اجرا می‌شود، CSS به شما نمی‌رسد و باید خودتان بپرسید:

const reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches

نمودارها این کار را از قبل انجام داده‌اند: useChartTheme() مقدار isAnimationActive را برمی‌گرداند که زیر این ترجیح false می‌شود.


موارد استفاده رایج

انیمیشن‌های آماده

هر کدام یک کلاس Tailwind است و به CSS سفارشی نیازی ندارد:

گروهکلاس‌ها
باز و بسته شدنanimate-accordion-down · animate-accordion-up · animate-collapsible-down · animate-collapsible-up
محو شدنanimate-fade-in · animate-fade-out
کشوییanimate-slide-up · animate-slide-down · animate-slide-in-right · animate-slide-in-left
مقیاسanimate-scale-in
وضعیت مداومanimate-shimmer · animate-pulse-subtle · animate-spin-slow · animate-job-indeterminate
خطاanimate-shake

این چند کلاس، تنها جای فیزیکیِ باقی‌مانده در سیستم‌اند

animate-slide-in-right و animate-slide-in-left هم نامشان فیزیکی است و هم با translateX پیاده شده‌اند، پس با جهت متن عوض نمی‌شوند — در RTL و LTR یکسان حرکت می‌کنند. بقیهٔ سیستم منطقی است (ms/me، start/end) و قانون ESLint no-physical-css-properties هم نام کلاس‌ها را می‌گیرد، ولی translateX داخل keyframe از دستش در می‌رود. اگر ورود باید از «ابتدای خط» باشد، خودتان با rtl:/ltr: انتخاب کنید یا یک transition روی خاصیت منطقی بنویسید.

حرکت در نمودار

import { useChartTheme } from '@partodata/ui'

const chartTheme = useChartTheme()
// chartTheme.isAnimationActive زیر prefers-reduced-motion برابر false می‌شود

در نمای چاپ یا خروجی PDF انیمیشن را خاموش کنید؛ وگرنه ممکن است فریم اول — یعنی نمودار خالی — روی کاغذ بیفتد.


چه نکنیم

  • انیمیشن روی چیزی که کاربر منتظرش نیست. اسکلتون که به داده تبدیل می‌شود، بله. کارت متریکی که خودش تکان بخورد، نه.
  • مدت بیش از ۳۰۰ میلی‌ثانیه برای المان کوچک. حس کندی می‌دهد، نه نرمی.
  • حرکت هم‌زمان در چند ناحیه. چشم یک کانون دارد؛ دو حرکت هم‌زمان یعنی هیچ‌کدام دیده نمی‌شود.
  • transition: all. هر خاصیت را نام ببرید، وگرنه رنگ و چیدمان با هم عوض می‌شوند و مرورگر layout را بی‌دلیل بازمی‌سازد.
  • انیمیشن به‌جای بازخورد. اگر عملیات طول می‌کشد، حالت بارگذاری نشان دهید؛ چرخاندن دکمه جای اسکلتون را نمی‌گیرد.

صفحات مرتبط