طراحی وب

Motion Design با CSS و Motion for React؛ رفتار و کاهش حرکت

هدف حرکت، CSS Transition، ورود و خروج در React، Reduced Motion و محدودیت عملکرد را بشناسید؛ همراه مثال کامل کارت با همه فایل‌ها و روش اجرا.

1404/11/14 4 دقیقه مطالعه 2,175 بازدید

وقتی جزئیات یک کارت باز می‌شود، حرکت می‌تواند نشان دهد محتوای تازه به همان کارت تعلق دارد. وقتی فرم ارسال می‌شود، تغییر وضعیت دکمه می‌تواند شروع پردازش را روشن کند. اما حرکت طولانی یا دائمی ممکن است انجام کار را کند کند یا خواندن متن را دشوار سازد. Motion Design باید یک تغییر معنادار را توضیح دهد؛ اضافه‌کردن انیمیشن به همه اجزا، جای طراحی وضعیت‌های درست را نمی‌گیرد.

برای انتخاب ابزار، ابتدا رفتار لازم را مشخص کنید. تغییر رنگ یک دکمه، ورود یک بخش شرطی، مرتب‌شدن فهرست و حرکت متصل به Scroll نیازهای یکسانی ندارند. CSS و کتابخانه JavaScript را صرفاً به «ساده» و «پیچیده» تقسیم نکنید. هزینه نگهداری، فناوری پروژه، کنترل زمان‌بندی و نیاز به هماهنگی با State هم مهم‌اند. یک افکت کوتاه و مستقل ممکن است اصلاً به Dependency تازه نیاز نداشته باشد.

نام فعلی کتابخانه را بشناسید

Motion for React نام فعلی کتابخانه‌ای است که قبلاً Framer Motion نامیده می‌شد. در مسیر فعلی، Package به نام motion نصب می‌شود و API React از motion/react وارد می‌شود. این نام با محصول طراحی Framer یکی نیست. آموزش قدیمی ممکن است هنوز import از framer-motion داشته باشد؛ فایل Package و نسخه همان پروژه را بخوانید و دو مسیر نصب را بدون تصمیم مشخص مخلوط نکنید.

وجود کتابخانه به معنی تضمین نرخ فریم، سرعت یا مناسب‌بودن هر انیمیشن نیست. حتی یک API درست هم می‌تواند روی صفحه سنگین یا دستگاه محدود نتیجه متفاوتی داشته باشد. از عبارت‌هایی مثل «همیشه GPU»، «بدون افت عملکرد» یا «هر انیمیشن ۲۰۰ تا ۳۰۰ میلی‌ثانیه» به‌عنوان قانون استفاده نکنید. نوع تغییر، فاصله حرکت و نیاز کاربر باید دلیل انتخاب تنظیمات باشند. نمونه پایین مقدار آموزشی دارد، نه استاندارد عمومی محصول.

هدف حرکت را با یک جمله بنویسید

برای هر Motion، یک جمله کوتاه بنویسید: این حرکت چه چیزی را برای کاربر روشن می‌کند؟ مثلاً «بازشدن جزئیات، ارتباط آن را با دکمه بالای کارت نشان می‌دهد». اگر پاسخ فقط «صفحه جذاب‌تر می‌شود» است، افکت را در برابر نیاز واقعی بررسی کنید. کاربر باید بدون دیدن حرکت هم اطلاعات و اقدام لازم را بفهمد. پیام موفقیت، انتخاب فعال و خطا نباید فقط با یک جابه‌جایی یا لرزش انتقال پیدا کنند.

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

CSS Transition برای تغییر مشخص

CSS Transition گذار بین مقدارهای یک خاصیت را کنترل می‌کند. اگر فقط رنگ، Opacity یا Transform یک کنترل تغییر می‌کند، این مسیر می‌تواند کافی باشد. بهتر است خاصیت‌های لازم را صریح بنویسید؛ transition: all ممکن است تغییر ناخواسته‌ای را هم وارد انیمیشن کند. Hover، Focus و Active را از هم تشخیص دهید؛ کاربر صفحه‌کلید لزوماً وارد حالت Hover نمی‌شود.

Transition وضعیت برنامه را ایجاد نمی‌کند؛ فقط نمایش تغییر را تنظیم می‌کند. اگر دکمه عملیات واقعی دارد، Loading، موفقیت و خطا باید از نتیجه همان عملیات بیایند. یک تغییر رنگ سریع نمی‌تواند تکمیل درخواست را ثابت کند. همچنین اگر عنصر بلافاصله از DOM حذف شود، صرف نوشتن CSS خروج الزاماً فرصت نمایش آن را ایجاد نمی‌کند. برای این رفتار به مدیریت حضور عنصر یا قرارداد مناسب CSS نیاز دارید.

React Motion را به State و حضور عنصر وصل می‌کند

در React، motion Componentها می‌توانند مقدار هدف را از State دریافت کنند. AnimatePresence نیز برای هماهنگی انیمیشن خروج هنگام حذف عنصر شرطی کاربرد دارد. Key باید هویت درست را نشان دهد؛ تغییر تصادفی آن می‌تواند به ایجاد دوباره عنصر و ازبین‌رفتن State منجر شود. خروج یک بخش را با پنهان‌شدن منطقی آن هماهنگ کنید، به‌خصوص وقتی داخل آن کنترل قابل Focus وجود دارد.

برای یک Panel تعاملی، بستن باید مشخص کند Focus به کجا می‌رود و کنترل‌های در حال خروج چه وضعیتی دارند. نمونه این مقاله داخل Panel فقط متن دارد تا این مسئله را با یک Dialog کامل اشتباه نگیریم. اگر پنجره Modal می‌سازید، مدیریت Focus، Escape و پس‌زمینه هم لازم است. زیبایی حرکت به‌تنهایی آن قراردادها را ایجاد نمی‌کند. برای الگوهای تعاملی، راهنمای الگوهای Component در React مرتبط است.

نمونه کامل: جزئیات کارت با امکان حذف حرکت

در یک پوشه تازه، package.json و index.html را در ریشه و main.jsx و style.css را داخل src بسازید. Node.js نسخه 22.12 یا جدیدتر و npm لازم است. نمونه از React شاخه 19، Motion شاخه 12 و Vite شاخه 7 استفاده می‌کند. همه فایل‌ها پایین آمده‌اند؛ تصویر، فونت یا API خارجی برای عملکرد نمونه لازم نیست. دسترسی به Registry فقط برای نصب Dependencyها لازم است.

با npm install وابستگی‌ها را نصب کنید و سپس npm run dev را اجرا کنید. نشانی محلی Vite را باز کنید. npm run build فایل‌های قابل انتشار را می‌سازد و npm run preview برای مشاهده محلی خروجی ساخته‌شده است. این برنامه فقط باز و بسته‌شدن یک توضیح را نشان می‌دهد؛ دکمه ذخیره پیام محلی می‌دهد و هیچ داده‌ای روی Server ذخیره نمی‌کند. Lockfile را برای تکرار نسخه‌های حل‌شده نگه دارید.

فایل package.json

{
  "name": "motion-card-demo",
  "version": "1.0.0",
  "private": true,
  "type": "module",
  "engines": { "node": ">=22.12.0" },
  "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" },
  "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", "motion": "^12.0.0" },
  "devDependencies": { "vite": "^7.0.0" }
}

فایل index.html

<!doctype html>
<html lang="fa" dir="rtl">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1">
  <title>نمونه Motion کارت</title></head>
<body><div id="root"></div><script type="module" src="/src/main.jsx"></script></body>
</html>

فایل src/main.jsx

import React, { useState, useId } from 'react';
import { createRoot } from 'react-dom/client';
import { motion, AnimatePresence, MotionConfig, useReducedMotion } from 'motion/react';
import './style.css';

function Demo() {
  const [open, setOpen] = useState(false);
  const [manualReduce, setManualReduce] = useState(false);
  const [message, setMessage] = useState('هیچ داده‌ای به Server ارسال نمی‌شود.');
  const systemReduce = useReducedMotion();
  const reduce = manualReduce || Boolean(systemReduce);
  const detailsId = useId();
  return (
    <main className={reduce ? 'demo reduce-motion' : 'demo'}>
      <h1>حرکت برای نمایش جزئیات</h1>
      <label><input type="checkbox" checked={manualReduce}
        onChange={event => setManualReduce(event.target.checked)} /> حذف حرکت نمونه</label>
      <article className="card">
        <h2>بسته بازبینی رابط</h2>
        <p>مشخصات را باز کنید؛ محتوا در حالت بدون حرکت هم در دسترس است.</p>
        <button type="button" aria-expanded={open} aria-controls={detailsId}
          onClick={() => setOpen(value => !value)}>{open ? 'بستن جزئیات' : 'نمایش جزئیات'}</button>
        <div id={detailsId}>
          <AnimatePresence initial={false}>
            {open && <motion.div key="details" className="details"
              initial={{ opacity: reduce ? 1 : 0, y: reduce ? 0 : 8 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: reduce ? 1 : 0, y: reduce ? 0 : 8 }}
              transition={{ duration: reduce ? 0 : .18, ease: 'easeOut' }}>
              <p>این بسته فرضی شامل بررسی ساختار فرم و متن پیام‌های خطاست. پنل فقط متن دارد.</p>
            </motion.div>}
          </AnimatePresence>
        </div>
        <button className="save" type="button"
          onClick={() => setMessage('پیام محلی: انتخاب نمونه نمایش داده شد؛ ذخیره بیرونی انجام نشد.')}>
          نمایش پیام انتخاب
        </button>
      </article>
      <p role="status">{message}</p>
    </main>
  );
}
const root = document.getElementById('root');
if (!root) throw new Error('Missing #root');
createRoot(root).render(<MotionConfig reducedMotion="user"><Demo /></MotionConfig>);

فایل src/style.css

* { box-sizing: border-box; }
body { margin: 0; padding: 1.5rem; font: 1rem/1.8 sans-serif; color: #172638; background: #f1f5fa; }
.demo { max-inline-size: 40rem; margin-inline: auto; }
.card { margin-block: 1.5rem; padding: 1.5rem; background: white; border: 1px solid #63768b; border-radius: .75rem; }
button { font: inherit; padding: .6rem 1rem; margin-block: .5rem; color: #fff; background: #164d86;
  border: 2px solid #164d86; border-radius: .35rem; transition: background-color .15s ease-out; }
button:hover { background-color: #123b65; }
button:focus-visible, input:focus-visible { outline: 3px solid #8b3600; outline-offset: 4px; }
.details { margin-block: .75rem; padding: 1rem; background: #edf3fa; }
.reduce-motion button { transition: none; }
@media (prefers-reduced-motion: reduce) { button { transition: none; } }

در مثال، گذار رنگ دکمه با CSS انجام می‌شود و ورود و خروج متن با Motion. Checkbox امکان حذف حرکت را مستقل از تنظیمات سیستم می‌دهد. دکمه بازکردن، aria-expanded را با State هماهنگ می‌کند و aria-controls به Container ثابت جزئیات اشاره دارد. حذف انیمیشن، محتوا یا قابلیت بازکردن را حذف نمی‌کند. این یک نمونه آموزشی محدود است و نتیجه آن ادعای انطباق کامل یا عملکرد تضمینی محسوب نمی‌شود.

Reduced Motion را به یک تنظیم فراموش‌شده تبدیل نکنید

prefers-reduced-motion ترجیح کاربر برای کاهش Motion را در CSS قابل استفاده می‌کند. این ترجیح با غیرفعال‌کردن همه تغییرهای UI یکی نیست؛ هنوز باید نتیجه عمل معلوم باشد. برای حرکت مکانی غیرضروری، نمایش مستقیم یا تغییر کوتاه غیرمکانی می‌تواند مناسب باشد. انتخاب را با هدف افکت هماهنگ کنید و تجربه بدون حرکت را به‌عنوان یک حالت کامل طراحی کنید.

در Motion، MotionConfig تنظیم عمومی فراهم می‌کند و useReducedMotion امکان تصمیم در Component را می‌دهد. کاهش Transform به معنی حذف خودکار همه Opacity یا CSS مستقل نیست. در نمونه، تصمیم Component زمان و جابه‌جایی را هم کنترل می‌کند و CSS دکمه با ترجیح مربوط هماهنگ می‌شود. اگر چند فناوری Motion دارید، مسئولیت هر لایه را روشن کنید.

حرکت طولانی و خودکار قرارداد دیگری دارد

کاروسل خودکار، پس‌زمینه متحرک و داده در حال حرکت با یک ورود کوتاه ناشی از کلیک متفاوت‌اند. معیار Pause, Stop, Hide برای شرایط مشخص حرکت یا به‌روزرسانی خودکار، امکان کنترل را مطرح می‌کند. آن را با یک قانون کلی «هر حرکت زیر پنج ثانیه مجاز است» جایگزین نکنید. نوع محتوا، زمان شروع و ارتباط آن با محتوای دیگر باید بررسی شود.

معیار Animation from Interactions نیز در سطح AAA درباره امکان غیرفعال‌کردن Motion ناشی از تعامل، با استثنای حرکت ضروری است. سطح معیار و دامنه آن را درست بیان کنید. حتی اگر پروژه هدف انطباق دیگری دارد، نیاز کاربر به کاهش حرکت همچنان برای تصمیم طراحی مرتبط است. افکت نباید خواندن متن یا کامل‌کردن یک کار ضروری را به تحمل حرکت وابسته کند.

عملکرد و نگهداری را جزو تصمیم قرار دهید

انیمیشن‌دادن Layout، افکت‌های بزرگ و تغییرهای پرتکرار می‌توانند هزینه متفاوتی داشته باشند. از یک نام Property نتیجه قطعی درباره سرعت نگیرید. حجم Dependency، تعداد عنصر و سناریوی دستگاه را همراه با نیاز بررسی کنید. در راهنمای Performance Budget می‌توانید این محدودیت‌ها را به تصمیم محصول وصل کنید. اگر CSS کار لازم را انجام می‌دهد، افزودن کتابخانه باید دلیل دیگری داشته باشد.

برای تحویل به تیم، Trigger، حالت آغاز و پایان، زمان، Easing، رفتار قطع و حالت کاهش حرکت را ثبت کنید. در Design Tokens می‌توان برخی تنظیمات مشترک را نگه داشت، ولی Token جای توضیح هدف رفتار را نمی‌گیرد. Motion مناسب، تغییر را قابل فهم‌تر می‌کند و در صورت حذف هم مسیر کاربر سالم می‌ماند. از یک Component محدود شروع کنید و تصمیم را با محتوای واقعی و وضعیت‌های کامل آن ادامه دهید.

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

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

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

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