حرکت و انیمیشن
مدت، 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-fast | 150ms | بازخورد فوری: hover، focus، تغییر رنگ |
--duration-normal | 200ms | ورود و خروج المان: dropdown، tooltip، sheet |
--duration-slow | 300ms | جابهجایی بزرگتر: drawer تمامقد، تغییر چیدمان |
--easing-standard | cubic-bezier(0.4, 0, 0.2, 1) | پیشفرض؛ ورود و خروج متعادل |
--easing-decelerate | cubic-bezier(0, 0, 0.2, 1) | ورود — سریع شروع میکند، آرام میایستد |
--easing-accelerate | cubic-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 را بیدلیل بازمیسازد.- انیمیشن بهجای بازخورد. اگر عملیات طول میکشد، حالت بارگذاری نشان دهید؛ چرخاندن دکمه جای اسکلتون را نمیگیرد.
صفحات مرتبط
- الگوهای بارگذاری — اسکلتون و اسپینر، جایی که حرکت واقعاً کار میکند
- دسترسیپذیری —
prefers-reduced-motionو اینکه چه چیزی باید خاموش شود - توکنهای طراحی — مرجع کامل و تولیدشدهٔ توکنها، شامل خانوادهٔ حرکت