توسعه فرانت‌اند

Motion for React؛ ورود، خروج و حرکت دسترس‌پذیر

نام فعلی Framer Motion، انیمیشن ورود و خروج، key و AnimatePresence و نمونه کامل پنل React با reduced motion و CSS.

1404/11/23 3 دقیقه مطالعه 2,242 بازدید

حرکت باید تغییر رابط را توضیح دهد

وقتی پنل جزئیات باز می‌شود، یک حرکت کوتاه می‌تواند ارتباط دکمه و محتوای تازه را نشان دهد. اگر همان پنل هر بار با چرخش و جهش بزرگ وارد شود، خواندن اطلاعات عقب می‌افتد. مسئلهٔ انیمیشن در React از همین تصمیم شروع می‌شود: کاربر چه تغییری را باید متوجه شود؟ کتابخانه اجرای حرکت را آسان می‌کند، ولی دلیل آن را انتخاب نمی‌کند. برای تغییر سادهٔ رنگ دکمه، CSS اغلب کافی است؛ هماهنگ کردن ورود، خروج و تغییر موقعیت چند عنصر ممکن است ابزار دیگری بخواهد.

نام فعلی کتابخانه‌ای که قبلاً Framer Motion شناخته می‌شد، Motion for React است. در نمونهٔ این نوشته package با نام motion نصب می‌شود و import از motion/react است. پروژهٔ قدیمی نباید صرفاً با تغییر نام import، بدون بررسی وابستگی و قرارداد نسخه، مهاجرت کند. راهنمای رسمی Motion مسیر شروع فعلی را توضیح می‌دهد. ادعای قدرتمندترین ابزار یا تضمین نرخ فریم در همهٔ دستگاه‌ها، معیار عملی انتخاب برای یک رابط مشخص نیست.

State مقصد را مشخص می‌کند

در رویکرد declarative، state برنامه می‌گوید پنل باز است یا بسته؛ ویژگی animate ظاهر مقصد را تعریف می‌کند. initial وضعیت شروع و transition نحوهٔ رسیدن به مقصد را تعیین می‌کنند. این تفکیک کمک می‌کند منطق سفارش یا فرم داخل حلقهٔ انیمیشن قرار نگیرد. اگر کاربر زودتر دوباره کلیک کرد، وضعیت برنامه باید همچنان روشن باشد. سرعت حرکت نباید تعیین کند آیا یک سفارش ثبت شده یا یک انتخاب معتبر است. داده و نمایش، قراردادهای جدا دارند.

ویژگی‌هایی مثل opacity و transform معمولاً برای حرکت کوتاه مناسب‌تر از تغییر پیوستهٔ موقعیت فیزیکی عناصر با top و left هستند، اما هزینهٔ واقعی به صفحه و دستگاه بستگی دارد. سایه‌های سنگین، محتوای بزرگ و لایه‌های متعدد هم می‌توانند هزینه داشته باشند. افزودن will-change به همهٔ عناصر راه‌حل عمومی نیست. بهتر است ابتدا عنصر و تغییر مورد نیاز را محدود کنید. هر حرکتی که اطلاعات را برای چند لحظه پنهان می‌کند باید توجیه داشته باشد؛ به‌خصوص اگر محتوا برای انجام کار ضروری است.

خروج با حذف فوری متفاوت است

React معمولاً عنصر شرطی را پس از تغییر state از درخت حذف می‌کند. اگر انتظار داریم حرکت خروج پیش از حذف کامل دیده شود، باید عمر نمایشی آن مدیریت شود. AnimatePresence برای همین بخش است. فرزند شرطی با key پایدار و motion component، وضعیت exit دارد. خود AnimatePresence باید هنگام تغییر شرط باقی بماند؛ گذاشتن آن داخل همان شرطی که حذف می‌شود، فرصتی برای هماهنگی خروج نمی‌دهد. مستندات AnimatePresence جزئیات این قرارداد را شرح داده است.

Key باید هویت عنصر را بیان کند. استفاده از شمارهٔ فعلی آرایه برای فهرستی که ترتیبش عوض می‌شود، می‌تواند خروج یک آیتم را به آیتم دیگری نسبت دهد. در پنل ثابت مثال، یک key ثابت کافی است. در فهرست کارها شناسهٔ واقعی هر کار لازم است. همچنین پایان انیمیشن خروج نباید تنها محل پاک کردن دادهٔ حساس یا انجام عملیات اصلی باشد. ممکن است حالت کاهش حرکت فعال باشد یا component مسیر دیگری برای حذف داشته باشد؛ منطق برنامه باید مستقل از جلوهٔ بصری درست بماند.

نمونهٔ کامل: پنل توضیح کوتاه

این نمونه به Node 24 و npm نیاز دارد. در یک پوشهٔ خالی package.json و index.html را در ریشه ذخیره کنید. پوشهٔ src را بسازید و main.jsx و styles.css را داخل آن قرار دهید. نام فایل‌های نمونه هنگام کپی باید به همین نام‌های مقصد تغییر کند. سپس npm install و npm run dev را اجرا کنید و نشانی اعلام‌شدهٔ Vite را باز کنید. npm run build خروجی نهایی را می‌سازد. محدودهٔ نسخه‌ها در package مشخص است؛ lock واقعی پس از نصب را برای تکرار نصب نگه دارید.

{"name":"motion-panel","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"vite","build":"vite build"},"dependencies":{"react":"^19.2.0","react-dom":"^19.2.0","motion":"^12.0.0"},"devDependencies":{"vite":"^8.0.0"}}
<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>پنل توضیح</title></head><body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>
import React, { useState } from 'react';
import { createRoot } from 'react-dom/client';
import { motion, AnimatePresence, useReducedMotion } from 'motion/react';
import './styles.css';
function App() {
  const [open, setOpen] = useState(false);
  const reduce = useReducedMotion();
  const moving = !reduce;
  return <main><h1>جزئیات سفارش</h1><p>توضیح تکمیلی با دکمه باز و بسته می‌شود.</p>
    <button type="button" aria-expanded={open} aria-controls="details" onClick={() => setOpen(!open)}>{open ? 'بستن توضیح' : 'نمایش توضیح'}</button>
    <AnimatePresence initial={false}>
      {open && <motion.section key="details" id="details" aria-labelledby="details-heading"
        initial={{ opacity: 0, y: moving ? 12 : 0 }}
        animate={{ opacity: 1, y: 0 }}
        exit={{ opacity: 0, y: moving ? 8 : 0 }}
        transition={{ duration: reduce ? 0 : .18 }}>
        <h2 id="details-heading">روش تحویل</h2><p>زمان تحویل پس از ثبت اطلاعات گیرنده مشخص می‌شود. این پنل نمونهٔ آموزشی است.</p>
      </motion.section>}
    </AnimatePresence>
    <p className="note">باز کردن توضیح، اطلاعات فرم را تغییر نمی‌دهد.</p>
  </main>;
}
createRoot(document.getElementById('root')).render(<App />);
:root { font-family: system-ui,sans-serif; color: #19303a; background: #eff3f5; }
body { margin: 0; padding: 2rem 1rem; }
main { max-width: 38rem; margin: auto; padding: 1.5rem; background: white; border-radius: 1rem; }
button { font: inherit; background: #075c55; color: white; border: 0; padding: .8rem 1rem; border-radius: .5rem; cursor: pointer; }
button:focus-visible { outline: 3px solid #c26500; outline-offset: 4px; }
section { margin-block-start: 1rem; padding: 1rem; background: #edf5f2; border: 1px solid #97b7af; border-radius: .5rem; }
section h2 { margin-block-start: 0; }
.note { color: #485f68; }

دکمه با aria-expanded وضعیت باز بودن را بیان می‌کند و aria-controls به پنل مرتبط است. محتوای پنل متن ساده است و control قابل focus ندارد؛ بنابراین هنگام بسته شدن، focus داخل عنصر در حال خروج نمی‌ماند. خود دکمه هم بیرون پنل باقی می‌ماند. اگر همین نمونه را به منوی دارای لینک، فرم یا dialog تبدیل کنید، مدیریت focus و تعامل در زمان خروج مسئولیت تازه‌ای می‌شود. حرکت خروج به تنهایی رفتار دسترس‌پذیر یک dialog را پیاده نمی‌کند.

در کد، ترجیح کاهش حرکت با useReducedMotion خوانده می‌شود. هنگام فعال بودن آن، جابه‌جایی حذف و زمان transition صفر می‌شود. پنل همچنان باز می‌شود و اطلاعاتش قابل خواندن است. وضعیت اولیهٔ AnimatePresence هم با initial=false از حرکت ناخواستهٔ نخستین حضور جلوگیری می‌کند. این مثال آموزشی API، اتصال بیرونی یا تصویر ندارد و گزارشی از اجرای آزمایش ارائه نمی‌دهد. نام elementها، id و selectorهای CSS در همان فایل‌ها مشخص‌اند تا ساختار لازم برای کپی پنهان نماند.

کاهش حرکت، بخشی از رفتار محصول است

کسی که کاهش حرکت را در سیستم خود انتخاب کرده، ممکن است به حرکت‌های بزرگ حساس باشد یا خواندن صفحهٔ آرام‌تر را ترجیح دهد. حذف تمام اطلاعات همراه با حذف انیمیشن، پاسخ درستی نیست. پیام اصلی باید با متن، رنگ قابل تشخیص و وضعیت control منتقل شود. راهنمای دسترس‌پذیری Motion تفاوت تنظیم عمومی reducedMotion و تصمیم سفارشی با hook را توضیح می‌دهد. در این نمونه تصمیم سفارشی داریم تا نتیجهٔ کد مستقیم دیده شود.

Opacity هم همیشه بی‌اثر نیست؛ متن محو در میانهٔ حرکت می‌تواند خواندن را دشوار کند. برای اطلاعات ضروری بهتر است زمان انتظار کوتاه یا صفر باشد. چشمک زدن، تکرار بی‌پایان و حرکت زمینه هنگام خواندن متن طولانی، نیازمند بررسی جدا هستند. نوشتهٔ دسترس‌پذیری از روز اول کمک می‌کند طراحی حرکت را کنار کنترل با keyboard و ساختار semantic ببینید. گزینهٔ کاهش حرکت نباید بعد از تمام شدن طراحی به یک شرط پراکنده در چند component تبدیل شود.

Variants و layout را با نیاز مشخص اضافه کنید

وقتی چند عنصر حالت‌های مشترک دارند، variants نامی برای وضعیت‌ها فراهم می‌کند. نام‌هایی مثل باز، بسته یا انتخاب‌شده، خواندن ارتباط state و ظاهر را آسان می‌کنند. اما ساخت یک object عظیم از تمام حرکت‌های سایت، مسئولیت componentها را مبهم می‌کند. رفتار محلی را نزدیک همان component نگه دارید و فقط قرارداد واقعاً مشترک را جدا کنید. تأخیر پیاپی ورود آیتم‌ها نیز باید با تعداد آیتم‌ها سنجیده شود؛ فهرست بلند نباید کاربر را منتظر آخرین مورد نگه دارد.

Layout animation تغییر اندازه یا موقعیت در layout را به یک انتقال بصری تبدیل می‌کند. کاربرد آن هنگام مرتب شدن کارت‌ها یا تغییر محتوای یک بلوک قابل فهم است. استفاده از layoutId برای ارتباط دو نمای یک هویت، نیازمند شناسهٔ درست و محدودهٔ مناسب است. این امکانات در مثال پنل لازم نیستند و اضافه نشده‌اند. راهنمای layout animation محدودیت‌ها و رفتار این قابلیت را برای مرحلهٔ بعد توضیح می‌دهد.

Hover تنها راه اعلام تعامل نیست

بزرگ شدن یک کارت در hover نمی‌گوید کاربر keyboard هم به آن دسترسی دارد. اگر کارت لینک است، باید عنصر لینک و نشانهٔ focus داشته باشد. اگر فقط متن است، ایجاد جلوه‌ای شبیه دکمه ممکن است انتظار اشتباه بسازد. Gestureهایی مانند tap و drag نیز معنای control را تعریف نمی‌کنند. در دستگاه لمسی hover هم قرارداد متفاوتی دارد. ابتدا رفتار عنصر را با HTML روشن کنید، سپس انیمیشن را به آن اضافه کنید. اثر بصری نباید جای برچسب و وضعیت انتخاب را بگیرد.

حرکت را در مسیر اصلی کاربر محدود کنید

در صفحهٔ ثبت سفارش، کاربر باید بتواند عنوان، خطا و نتیجه را سریع بخواند. حرکت مناسب شاید فقط توضیح تغییر مرحله باشد. تأخیر نمایشی برای درخواست شبکه، وضعیت واقعی درخواست را عوض نمی‌کند؛ طراحی آن را در وضعیت بارگذاری و Skeleton جدا بررسی کنید. اگر باز شدن پنل state مشترک را تغییر می‌دهد، مرز آن با دادهٔ دامنه باید معلوم باشد؛ مقالهٔ مدیریت state با Zustand این تفکیک را ادامه می‌دهد.

برای نگهداری بلندمدت، مقادیر حرکت را همان‌قدر مستند کنید که رنگ و فاصله را مشخص می‌کنید: کدام تغییر حرکت دارد، چقدر طول می‌کشد و در کاهش حرکت چه اتفاقی می‌افتد. این تصمیم‌ها از اضافه کردن جلوه‌های بیشتر مفیدترند. اگر پنل بدون انیمیشن هم قابل فهم و قابل استفاده باشد، حرکت می‌تواند توضیح کوتاهی روی رفتار درست اضافه کند. هر جا جلوه فهمیدن نتیجه را عقب می‌اندازد، ساده کردن آن تصمیم معقولی است.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!