Motion لـ React: بناء رسوم متحركة وإيماءات وانتقالات بمستوى إنتاجي

مقدمة
تحوّل الرسوم المتحركة واجهة المستخدم الجيدة إلى واجهة رائعة. فهي توجه المستخدمين عبر تغييرات الحالة، وتقدم ملاحظات على التفاعلات، وتجعل التطبيقات تبدو حية. Motion (المعروفة سابقاً بـ Framer Motion) هي أشهر مكتبة رسوم متحركة لـ React، تستخدمها شركات مثل Vercel وLinear وStripe لإنشاء واجهات سلسة وعالية الأداء.
في هذا الدليل، ستتعلم Motion من الصفر. بدءاً من تأثيرات الظهور البسيطة، ستتقدم عبر الإيماءات ورسوم التخطيط المتحركة وتأثيرات التمرير والتسلسلات المعقدة المنسقة. في النهاية، ستكون قد بنيت معرض بطاقات منتجات متحرك بالكامل مع تأثيرات تحوم تفاعلية وانتقالات تخطيط سلسة وكشف عند التمرير.
ما ستتعلمه
- تثبيت وتهيئة Motion في مشروع Next.js أو React
- تحريك المكونات باستخدام مكون
motionوخاصيةanimate - إنشاء إيماءات تفاعلية: التحوم والنقر والسحب والتركيز
- بناء رسوم تخطيط متحركة تنتقل بسلاسة بين الحالات
- استخدام
AnimatePresenceلرسوم الدخول والخروج - تنفيذ رسوم متحركة مدفوعة بالتمرير باستخدام
useScrollوuseTransform - تنسيق تسلسلات معقدة مع المتغيرات وتأثيرات التتابع
- تحسين أداء الرسوم المتحركة للإنتاج
المتطلبات الأساسية
قبل البدء، تأكد من أن لديك:
- Node.js 18+ مثبت على جهازك
- معرفة أساسية بـ React وTypeScript
- إلمام بـ تحولات وانتقالات CSS (مفيد لكن غير مطلوب)
- محرر أكواد (يُنصح بـ VS Code)
الخطوة 1: إعداد المشروع
أنشئ مشروع Next.js جديد وثبّت Motion:
npx create-next-app@latest motion-demo --typescript --tailwind --app --src-dir
cd motion-demoثبّت مكتبة Motion:
npm install motionMotion الإصدار 11 وما بعده يُنشر تحت اسم حزمة motion (حزمة framer-motion لا تزال تعمل لكنها تُعيد التوجيه إلى motion). واجهة البرمجة نفسها — فقط اسم الحزمة تغيّر.
تحقق من التثبيت بفحص package.json:
{
"dependencies": {
"motion": "^11.18.0",
"next": "^15.2.0",
"react": "^19.0.0"
}
}الخطوة 2: أول رسم متحرك لك
استبدل محتويات src/app/page.tsx بمكون متحرك بسيط:
"use client";
import { motion } from "motion/react";
export default function Home() {
return (
<main className="flex min-h-screen items-center justify-center bg-gray-950">
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="rounded-2xl bg-white p-8 shadow-2xl"
>
<h1 className="text-2xl font-bold text-gray-900">
مرحباً، Motion!
</h1>
<p className="mt-2 text-gray-600">
هذه البطاقة ظهرت تدريجياً وانزلقت للأعلى بسلاسة.
</p>
</motion.div>
</main>
);
}شغّل خادم التطوير بـ npm run dev وافتح http://localhost:3000. سترى البطاقة تظهر تدريجياً وتنزلق للأعلى عند تحميل الصفحة.
كيف يعمل
motion.divهو بديل مباشر لـ<div>يدعم خصائص الرسوم المتحركةinitialتحدد حالة البداية (غير مرئي، منزاح 40 بكسل للأسفل)animateتحدد الحالة المستهدفة (مرئي بالكامل، الموضع الأصلي)transitionتتحكم في التوقيت والتخفيف
يقوم Motion تلقائياً بالاستيفاء بين قيم initial وanimate، ويتعامل مع كل منطق requestAnimationFrame والتحولات المسرّعة بالـ GPU نيابةً عنك.
الخطوة 3: فيزياء النوابض وأنواع الانتقالات
يدعم Motion ثلاثة أنواع من الانتقالات: tween (مبني على المدة)، وspring (مبني على الفيزياء)، وinertia (مبني على الزخم). رسوم النوابض المتحركة تبدو الأكثر طبيعية لتفاعلات الواجهة.
"use client";
import { motion } from "motion/react";
export default function SpringDemo() {
return (
<div className="flex min-h-screen items-center justify-center gap-8 bg-gray-950">
{/* Tween: قابل للتنبؤ، مبني على المدة */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "tween", duration: 0.5 }}
className="h-24 w-24 rounded-2xl bg-blue-500"
/>
{/* Spring: طبيعي، مبني على الفيزياء */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
className="h-24 w-24 rounded-2xl bg-green-500"
/>
{/* Spring مع ارتداد */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", bounce: 0.5 }}
className="h-24 w-24 rounded-2xl bg-purple-500"
/>
</div>
);
}معاملات النوابض
| المعامل | الوصف | القيمة الافتراضية |
|---|---|---|
stiffness | مدى صلابة النابض (أعلى = أسرع) | 100 |
damping | مقدار المقاومة (أعلى = تذبذب أقل) | 10 |
mass | وزن العنصر المتحرك | 1 |
bounce | اختصار: 0 = بدون ارتداد، 1 = ارتداد كبير | 0.25 |
نصيحة: لمعظم رسوم الواجهة المتحركة، type: "spring" بالقيم الافتراضية يعمل بشكل مثالي. عدّل المعاملات فقط عندما لا يتطابق الإحساس الافتراضي مع نية التصميم.
الخطوة 4: الإيماءات التفاعلية
يجعل Motion من السهل جداً إضافة رسوم متحركة للتحوم والنقر والسحب والتركيز. هذه الخصائص تستجيب لتفاعلات المستخدم بدون أي إدارة حالة.
التحوم والنقر
"use client";
import { motion } from "motion/react";
export default function GestureDemo() {
return (
<div className="flex min-h-screen items-center justify-center gap-8 bg-gray-950">
<motion.button
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6" }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="rounded-xl bg-blue-600 px-8 py-4 text-lg font-semibold text-white"
>
اضغط هنا
</motion.button>
<motion.div
whileHover={{
rotate: 5,
boxShadow: "0 25px 50px -12px rgba(0,0,0,0.5)",
}}
whileTap={{ rotate: -5, scale: 0.9 }}
className="flex h-32 w-32 cursor-pointer items-center justify-center rounded-2xl bg-gradient-to-br from-pink-500 to-orange-400 text-white font-bold"
>
تفاعل
</motion.div>
</div>
);
}السحب
يدعم Motion السحب الكامل مع القيود والحدود المرنة والزخم:
"use client";
import { motion } from "motion/react";
import { useRef } from "react";
export default function DragDemo() {
const constraintsRef = useRef<HTMLDivElement>(null);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950">
{/* حاوية حدود السحب */}
<motion.div
ref={constraintsRef}
className="relative flex h-96 w-96 items-center justify-center rounded-3xl border-2 border-dashed border-gray-700"
>
<p className="absolute top-4 text-sm text-gray-500">
اسحب الكرة
</p>
<motion.div
drag
dragConstraints={constraintsRef}
dragElastic={0.2}
dragTransition={{ bounceStiffness: 300, bounceDamping: 20 }}
whileDrag={{ scale: 1.2, cursor: "grabbing" }}
className="h-20 w-20 cursor-grab rounded-full bg-gradient-to-br from-violet-500 to-cyan-400 shadow-lg"
/>
</motion.div>
</div>
);
}خصائص السحب الرئيسية:
dragتمكّن السحب (trueأو"x"أو"y"لقفل المحور)dragConstraintsتحدّ من الحركة (مرجع أو قيم بكسل)dragElasticتتحكم في مدى السحب خارج القيود (0-1)whileDragتطبّق أنماط أثناء السحب
الخطوة 5: المتغيرات والتنسيق
عندما تحتاج لتحريك عناصر أبناء متعددة بالتتابع، تتيح لك المتغيرات تعريف حالات رسوم متحركة مسماة ونشرها عبر شجرة المكونات.
"use client";
import { motion } from "motion/react";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20, scale: 0.95 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring", stiffness: 300, damping: 24 },
},
};
const features = [
{ title: "سريع", description: "رسوم متحركة مسرّعة بالـ GPU بمعدل 60 إطار/ثانية" },
{ title: "بسيط", description: "واجهة برمجة تصريحية بدون كود معياري" },
{ title: "قوي", description: "دعم الإيماءات والتخطيط والتمرير" },
{ title: "صغير", description: "قابل للتقليم، يشحن فقط ما تستخدمه" },
];
export default function StaggerDemo() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid max-w-2xl grid-cols-2 gap-4"
>
{features.map((feature) => (
<motion.div
key={feature.title}
variants={itemVariants}
className="rounded-xl bg-gray-900 p-6 border border-gray-800"
>
<h3 className="text-lg font-bold text-white">{feature.title}</h3>
<p className="mt-1 text-sm text-gray-400">{feature.description}</p>
</motion.div>
))}
</motion.div>
</div>
);
}كيف تنتشر المتغيرات
- الأب
motion.divيحددinitial="hidden"وanimate="visible" - الأبناء الذين لديهم
variantsيرثون هذه الحالات تلقائياً staggerChildren: 0.1يضيف تأخير 100 مللي ثانية بين كل رسم متحرك للأبناءdelayChildren: 0.2ينتظر 200 مللي ثانية قبل بدء أول ابن
هذا النمط مثالي لتحريك القوائم والشبكات وقوائم التنقل وأي مجموعة من العناصر المرتبطة.
الخطوة 6: AnimatePresence لرسوم الخروج
يزيل React العناصر من DOM فوراً. يعترض AnimatePresence هذا ويسمح لرسوم الخروج بالاكتمال قبل الإزالة.
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
const notifications = [
{ id: 1, text: "رسالة جديدة وصلت", color: "bg-blue-500" },
{ id: 2, text: "تم رفع الملف بنجاح", color: "bg-green-500" },
{ id: 3, text: "تمت معالجة الدفع", color: "bg-purple-500" },
{ id: 4, text: "اكتمل النشر", color: "bg-orange-500" },
];
export default function ExitDemo() {
const [items, setItems] = useState(notifications);
const removeItem = (id: number) => {
setItems((prev) => prev.filter((item) => item.id !== id));
};
const resetItems = () => setItems(notifications);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
<div className="w-full max-w-md space-y-3">
<AnimatePresence mode="popLayout">
{items.map((item) => (
<motion.div
key={item.id}
layout
initial={{ opacity: 0, x: -40, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 40, scale: 0.95 }}
transition={{ type: "spring", stiffness: 350, damping: 25 }}
className="flex items-center justify-between rounded-xl bg-gray-900 p-4 border border-gray-800"
>
<div className="flex items-center gap-3">
<div className={`h-3 w-3 rounded-full ${item.color}`} />
<span className="text-white">{item.text}</span>
</div>
<button
onClick={() => removeItem(item.id)}
className="text-gray-500 hover:text-red-400 transition-colors"
>
تجاهل
</button>
</motion.div>
))}
</AnimatePresence>
{items.length === 0 && (
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
onClick={resetItems}
className="w-full rounded-xl bg-blue-600 py-3 text-white font-semibold"
>
إعادة تعيين الإشعارات
</motion.button>
)}
</div>
</div>
);
}أوضاع AnimatePresence
| الوضع | السلوك |
|---|---|
"sync" (افتراضي) | رسوم الخروج والدخول تحدث معاً |
"wait" | ينتظر اكتمال الخروج قبل الدخول |
"popLayout" | العناصر الخارجة تُزال من تدفق التخطيط |
خاصية layout على كل عنصر تضمن أن العناصر المتبقية تعيد تموضعها بسلاسة عند إزالة عنصر شقيق.
الخطوة 7: رسوم التخطيط المتحركة
رسوم التخطيط المتحركة هي واحدة من أقوى ميزات Motion. بإضافة خاصية layout، تنتقل العناصر بسلاسة بين تخطيطات CSS المختلفة — بدون حسابات إحداثيات يدوية.
"use client";
import { useState } from "react";
import { motion } from "motion/react";
export default function LayoutDemo() {
const [selected, setSelected] = useState<string | null>(null);
const items = ["تصميم", "تطوير", "نشر", "مراقبة"];
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
<div className="flex gap-2 rounded-2xl bg-gray-900 p-2">
{items.map((item) => (
<button
key={item}
onClick={() => setSelected(item)}
className="relative rounded-xl px-6 py-3 text-sm font-medium text-white outline-none"
>
{selected === item && (
<motion.div
layoutId="active-tab"
className="absolute inset-0 rounded-xl bg-blue-600"
transition={{ type: "spring", stiffness: 380, damping: 30 }}
/>
)}
<span className="relative z-10">{item}</span>
</button>
))}
</div>
</div>
);
}سحر layoutId
عندما تعطي عناصر متعددة نفس layoutId، يعاملها Motion كعنصر واحد عبر عمليات العرض. عندما يُزال أحدها ويُضاف آخر، يحرّك Motion بين مواقعها وأحجامها تلقائياً. هذه هي الطريقة لإنشاء:
- انتقالات عناصر مشتركة (مثل مؤشر التبويب أعلاه)
- توسيع من بطاقة إلى نافذة
- رسوم إعادة ترتيب القوائم
مثال توسيع البطاقة
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
const cards = [
{ id: "1", title: "التحليلات", description: "تتبع سلوك المستخدم ومقاييس التفاعل عبر منصتك.", color: "from-blue-500 to-cyan-400" },
{ id: "2", title: "الأمان", description: "تشفير شامل وبنية تحتية متوافقة مع معايير الامتثال.", color: "from-purple-500 to-pink-400" },
{ id: "3", title: "السرعة", description: "منشور على الحافة عالمياً مع أوقات استجابة أقل من 100 مللي ثانية.", color: "from-orange-500 to-yellow-400" },
];
export default function CardExpansion() {
const [selectedId, setSelectedId] = useState<string | null>(null);
const selectedCard = cards.find((c) => c.id === selectedId);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
<div className="grid grid-cols-3 gap-4">
{cards.map((card) => (
<motion.div
key={card.id}
layoutId={`card-${card.id}`}
onClick={() => setSelectedId(card.id)}
className={`cursor-pointer rounded-2xl bg-gradient-to-br ${card.color} p-6`}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.98 }}
>
<motion.h3
layoutId={`title-${card.id}`}
className="text-xl font-bold text-white"
>
{card.title}
</motion.h3>
</motion.div>
))}
</div>
<AnimatePresence>
{selectedId && selectedCard && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 0.5 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
className="fixed inset-0 bg-black"
/>
<motion.div
layoutId={`card-${selectedId}`}
className={`fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md rounded-2xl bg-gradient-to-br ${selectedCard.color} p-8`}
>
<motion.h3
layoutId={`title-${selectedId}`}
className="text-2xl font-bold text-white"
>
{selectedCard.title}
</motion.h3>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="mt-4 text-white/90"
>
{selectedCard.description}
</motion.p>
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
className="mt-6 rounded-lg bg-white/20 px-4 py-2 text-white font-medium backdrop-blur"
>
إغلاق
</motion.button>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}ينشئ هذا توسعاً سلساً من البطاقة إلى النافذة بدون أي تتبع يدوي للموضع. يحسب Motion رسم FLIP المتحرك تلقائياً.
الخطوة 8: الرسوم المتحركة المدفوعة بالتمرير
يوفر Motion خطافات لإنشاء رسوم متحركة تستجيب لموضع التمرير. هذا مثالي لأقسام البطل ومؤشرات التقدم وتأثيرات المنظور والكشف عند التمرير.
مؤشر تقدم التمرير
"use client";
import { motion, useScroll, useSpring } from "motion/react";
export default function ScrollProgress() {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001,
});
return (
<>
<motion.div
style={{ scaleX }}
className="fixed top-0 left-0 right-0 z-50 h-1 origin-left bg-blue-500"
/>
<main className="space-y-8 p-8">
{Array.from({ length: 20 }).map((_, i) => (
<div
key={i}
className="mx-auto h-48 max-w-2xl rounded-2xl bg-gray-900 border border-gray-800"
/>
))}
</main>
</>
);
}الكشف عند التمرير
"use client";
import { motion, useInView } from "motion/react";
import { useRef } from "react";
function RevealSection({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 60 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 60 }}
transition={{ duration: 0.7, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
export default function ScrollReveal() {
const sections = [
{ title: "صمّم", text: "أنشئ واجهات جميلة مع Motion" },
{ title: "حرّك", text: "أضف رسوم متحركة سائلة لكل تفاعل" },
{ title: "انشر", text: "انشر رسوم متحركة عالية الأداء للإنتاج" },
];
return (
<main className="min-h-screen bg-gray-950 p-8">
<div className="mx-auto max-w-2xl space-y-32 py-32">
{sections.map((section) => (
<RevealSection key={section.title}>
<h2 className="text-4xl font-bold text-white">{section.title}</h2>
<p className="mt-4 text-xl text-gray-400">{section.text}</p>
</RevealSection>
))}
</div>
</main>
);
}تأثير المنظور
"use client";
import { motion, useScroll, useTransform } from "motion/react";
import { useRef } from "react";
export default function ParallaxHero() {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const y = useTransform(scrollYProgress, [0, 1], [0, 200]);
const opacity = useTransform(scrollYProgress, [0, 0.8], [1, 0]);
const scale = useTransform(scrollYProgress, [0, 1], [1, 0.85]);
return (
<div ref={ref} className="relative h-screen overflow-hidden bg-gray-950">
<motion.div
style={{ y, opacity, scale }}
className="flex h-full flex-col items-center justify-center"
>
<h1 className="text-6xl font-bold text-white">بطل متحرك</h1>
<p className="mt-4 text-xl text-gray-400">مرّر للأسفل لرؤية التأثير</p>
</motion.div>
</div>
);
}الخطوة 9: بناء معرض منتجات متحرك كامل
لنجمع كل شيء في مكون حقيقي — معرض بطاقات منتجات متحرك مع دخول متتابع وتأثيرات تحوم وعروض تفصيلية قابلة للتوسيع.
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
interface Product {
id: string;
name: string;
price: string;
category: string;
gradient: string;
description: string;
}
const products: Product[] = [
{
id: "1",
name: "سماعات احترافية",
price: "$299",
category: "صوت",
gradient: "from-violet-600 to-indigo-600",
description:
"سماعات لاسلكية فاخرة مع إلغاء ضوضاء نشط وبطارية 40 ساعة وصوت بجودة الاستوديو.",
},
{
id: "2",
name: "شاشة ألترا",
price: "$899",
category: "عرض",
gradient: "from-cyan-600 to-blue-600",
description:
"شاشة 32 بوصة 4K مع HDR1000 ومعدل تحديث 165 هرتز وألوان معايرة مصنعياً للمحترفين المبدعين.",
},
{
id: "3",
name: "لوحة مفاتيح ميكانيكية",
price: "$179",
category: "إدخال",
gradient: "from-orange-600 to-red-600",
description:
"لوحة مفاتيح ميكانيكية قابلة للتبديل السريع مع إضاءة RGB لكل مفتاح وتصميم حشية وأغطية مفاتيح PBT فاخرة.",
},
{
id: "4",
name: "فأرة مريحة",
price: "$129",
category: "إدخال",
gradient: "from-emerald-600 to-teal-600",
description:
"فأرة عمودية مريحة مع مستشعر 8K DPI وبلوتوث 5.3 وأزرار جانبية قابلة للتخصيص.",
},
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.08, delayChildren: 0.1 },
},
};
const cardVariants = {
hidden: { opacity: 0, y: 30, scale: 0.96 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring", stiffness: 300, damping: 24 },
},
};
export default function ProductGallery() {
const [selectedId, setSelectedId] = useState<string | null>(null);
const selectedProduct = products.find((p) => p.id === selectedId);
return (
<div className="min-h-screen bg-gray-950 p-8">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-12 text-center text-4xl font-bold text-white"
>
المنتجات المميزة
</motion.h1>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="mx-auto grid max-w-5xl grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4"
>
{products.map((product) => (
<motion.div
key={product.id}
layoutId={`product-${product.id}`}
variants={cardVariants}
onClick={() => setSelectedId(product.id)}
whileHover={{ y: -8, transition: { type: "spring", stiffness: 400 } }}
whileTap={{ scale: 0.97 }}
className="cursor-pointer overflow-hidden rounded-2xl bg-gray-900 border border-gray-800"
>
<div
className={`h-40 bg-gradient-to-br ${product.gradient} flex items-center justify-center`}
>
<motion.span
layoutId={`emoji-${product.id}`}
className="text-5xl"
>
{product.category === "صوت"
? "🎧"
: product.category === "عرض"
? "🖥️"
: product.category === "إدخال"
? "⌨️"
: "🖱️"}
</motion.span>
</div>
<div className="p-4">
<motion.h3
layoutId={`name-${product.id}`}
className="font-bold text-white"
>
{product.name}
</motion.h3>
<motion.span
layoutId={`price-${product.id}`}
className="mt-1 block text-sm text-gray-400"
>
{product.price}
</motion.span>
</div>
</motion.div>
))}
</motion.div>
<AnimatePresence>
{selectedId && selectedProduct && (
<>
<motion.div
key="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 0.6 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
className="fixed inset-0 z-40 bg-black"
/>
<motion.div
key="modal"
layoutId={`product-${selectedId}`}
className="fixed z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg overflow-hidden rounded-2xl bg-gray-900 border border-gray-800"
>
<div
className={`h-48 bg-gradient-to-br ${selectedProduct.gradient} flex items-center justify-center`}
>
<motion.span
layoutId={`emoji-${selectedId}`}
className="text-7xl"
>
{selectedProduct.category === "صوت"
? "🎧"
: selectedProduct.category === "عرض"
? "🖥️"
: selectedProduct.category === "إدخال"
? "⌨️"
: "🖱️"}
</motion.span>
</div>
<div className="p-6">
<motion.h3
layoutId={`name-${selectedId}`}
className="text-2xl font-bold text-white"
>
{selectedProduct.name}
</motion.h3>
<motion.span
layoutId={`price-${selectedId}`}
className="mt-1 block text-lg text-gray-400"
>
{selectedProduct.price}
</motion.span>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.2 } }}
exit={{ opacity: 0 }}
className="mt-4 text-gray-300"
>
{selectedProduct.description}
</motion.p>
<motion.button
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0, transition: { delay: 0.3 } }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
className="mt-6 w-full rounded-xl bg-white/10 py-3 text-white font-semibold hover:bg-white/20 transition-colors"
>
إغلاق
</motion.button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}يوضح هذا المعرض:
- دخول متتابع مع
containerVariantsوcardVariants - تأثير رفع عند التحوم مع
whileHover - انتقال تخطيط مشترك من البطاقة إلى النافذة مع
layoutId - رسم خروج متحرك مع
AnimatePresence - كشف محتوى متأخر داخل النافذة الموسعة
الخطوة 10: تحسين الأداء
صُمم Motion ليكون عالي الأداء، لكن هناك أنماط للحفاظ على سلاسة الرسوم المتحركة في الإنتاج.
1. حرّك Transform وOpacity فقط
خصائص التحويل والشفافية المسرّعة بالـ GPU هي الأرخص في التحريك. تجنب تحريك width وheight وpadding أو top/left — استخدم scale وx وy وopacity بدلاً من ذلك.
// بطيء: يُفعّل إعادة حساب التخطيط
<motion.div animate={{ width: 200, height: 200 }} />
// سريع: تحويل مسرّع بالـ GPU
<motion.div animate={{ scale: 1.5 }} />2. استخدم layout بحكمة
خاصية layout قوية لكنها مكلفة للقوائم الكبيرة. إذا كنت تحتاج فقط تغييرات الموضع، استخدم layout="position" بدلاً من layout={true}:
// يحرّك الموضع والحجم ونصف القطر
<motion.div layout />
// يحرّك الموضع فقط (أرخص)
<motion.div layout="position" />3. قلّل الحركة لإمكانية الوصول
احترم دائماً تفضيل المستخدم لتقليل الحركة:
"use client";
import { motion, useReducedMotion } from "motion/react";
export default function AccessibleAnimation() {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0.01 : 0.6 }}
className="rounded-2xl bg-gray-900 p-8"
>
<p className="text-white">يحترم prefers-reduced-motion</p>
</motion.div>
);
}4. تحميل كسول لمكونات Motion
إذا كانت الرسوم المتحركة تُستخدم فقط في صفحات محددة، استورد Motion ديناميكياً لتقليل حجم الحزمة:
import dynamic from "next/dynamic";
const AnimatedHero = dynamic(() => import("@/components/AnimatedHero"), {
ssr: false,
loading: () => <div className="h-screen bg-gray-950" />,
});5. استخدم will-change باعتدال
يطبّق Motion بالفعل will-change: transform عند الحاجة. لا تضفه يدوياً إلا بعد التحليل والتأكد من أنه يساعد — الاستخدام المفرط يستهلك ذاكرة GPU.
استكشاف الأخطاء وإصلاحها
المشاكل الشائعة
وميض الرسوم المتحركة عند أول عرض مع Next.js App Router
أضف "use client" لأي مكون يستخدم Motion. لا يمكن لـ Server Components استخدام خطافات أو خصائص إيماءات Motion.
رسوم التخطيط تسبب قفز العناصر
تأكد من أن العناصر الأب لها أبعاد مستقرة. تجنب العرض بالنسب المئوية على العناصر التي لها layout — استخدم أحجام ثابتة أو مرنة بدلاً من ذلك.
رسم الخروج لا يعمل
تأكد من:
AnimatePresenceيحيط بالعناصر الشرطية- كل ابن لديه خاصية
keyفريدة - خاصية
exitمعرّفة على الابن المباشر لـAnimatePresence
حجم الحزمة كبير جداً
Motion v11+ قابل للتقليم بالكامل. استورد فقط ما تحتاجه:
// جيد: يستورد فقط ما هو مطلوب
import { motion, AnimatePresence } from "motion/react";
// تجنب: استيراد كل شيء
import * as Motion from "motion/react";الخطوات التالية
الآن بعد فهمك لأساسيات Motion، إليك بعض الاتجاهات للاستكشاف:
- انتقالات الصفحات — ادمج Motion مع انتقالات تخطيط Next.js للتنقل السلس
- رسوم SVG المتحركة — استخدم
motion.pathوpathLengthلتأثيرات الرسم - تحولات ثلاثية الأبعاد — استخدم
rotateXوrotateYوperspectiveلتأثيرات قلب البطاقات - منظور مرتبط بالتمرير — ابنِ صفحات هبوط غامرة مع
useScrollوuseTransform - خطافات رسوم قابلة لإعادة الاستخدام — استخرج الأنماط في خطافات مخصصة مثل
useFadeInأوuseStagger
الخلاصة
يقدم Motion رسوم متحركة بمستوى إنتاجي لـ React مع واجهة برمجة بسيطة وتصريحية. تعلمت كيف تنشئ رسوم متحركة أساسية مع النوابض والتخفيفات، وتبني إيماءات تفاعلية للتحوم والنقر والسحب، وتنسّق تسلسلات معقدة بالمتغيرات، وتتعامل مع انتقالات الدخول والخروج مع AnimatePresence، وتحرّك تغييرات التخطيط تلقائياً، وتبني تأثيرات مدفوعة بالتمرير.
المفتاح لرسوم واجهة مستخدم رائعة هو الاعتدال — حرّك بهدف، واحترم تفضيلات تقليل الحركة، وحسّن الأداء دائماً. ابدأ بمعرض المنتجات الذي بنيته في هذا الدليل وجرّب إضافة Motion لمشاريعك الخاصة.
ناقش مشروعك معنا
نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.
دعنا نجد أفضل الحلول لاحتياجاتك.
مقالات ذات صلة

بناء تطبيق كامل يعمل بالوقت الحقيقي باستخدام Convex و Next.js 15
تعلّم كيفية بناء تطبيق كامل يعمل بالوقت الحقيقي باستخدام Convex و Next.js 15. يغطي هذا الدليل تصميم المخططات والاستعلامات والتعديلات والاشتراكات الفورية والمصادقة ورفع الملفات — مع أمان أنواع شامل.

بناء تطبيق كامل مع Firebase و Next.js 15: المصادقة، Firestore والتحديث الفوري
تعلم كيفية بناء تطبيق full-stack مع Next.js 15 و Firebase. يغطي هذا الدليل المصادقة، Firestore، التحديثات الفورية، Server Actions والنشر على Vercel.

إضافة المصادقة لتطبيق Next.js 15 باستخدام Auth.js v5: البريد الإلكتروني وOAuth والتحكم بالأدوار
تعلم كيفية إضافة نظام مصادقة جاهز للإنتاج لتطبيق Next.js 15 باستخدام Auth.js v5. يغطي هذا الدليل الشامل تسجيل الدخول عبر Google OAuth وبيانات الاعتماد بالبريد الإلكتروني وكلمة المرور والمسارات المحمية والتحكم بالوصول حسب الأدوار.