React: الانتقالات والرسوم المتحركة
آخر تحديث: 2026-08-26
كان توم قد انتهى لتوه من مشروع لوحة بيانات، لكن مدير المنتج أشار إلى أن انتقالات الصفحات كانت مفاجئة للغاية — حيث كانت البيانات تظهر مصحوبة بـ«وميض» عند تحميل القائمة، وكانت النوافذ المنبثقة تفتح وتغلق على الفور. أدرك توم أن توفر جميع الميزات لا يعني بالضرورة تجربة مستخدم جيدة. كان عليه إضافة رسوم متحركة انتقالية سلسة إلى الصفحات.
1. ما ستتعلمه
- الاستخدامات الأساسية والاختلافات بين انتقالات CSS والرسوم المتحركة
- مكون الحركة في Framer Motion و AnimatePresence
- الرسوم المتحركة الخاصة بالتثبيت/إلغاء التثبيت
- الرسوم المتحركة الجماعية لعناصر القائمة (الرسوم المتحركة للتخطيط)
- رسوم متحركة تفاعلية تعتمد على الإيماءات (السحب، التمرير فوق العنصر)
2. الرسوم التخطيطية المفاهيمية
سيساعدك الرسم التوضيحي التالي على اتخاذ قرارات سريعة عند اختيار خيار الرسوم المتحركة:
flowchart TD
A[Animation effects are needed] --> B{Effect Complexity}
B -->|Simple Transition| C[CSS Transition]
B -->|Keyframe Animation| D[CSS Animation]
B -->|Complex Interactions| E[Framer Motion]
C --> C1["hover / focus Effect"]
C --> C2["opacity / transform Fade Animation"]
C --> C3["Changes to a Single Attribute"]
D --> D1["@keyframes Defining Multi-Stage"]
D --> D2["Infinite Loop Animation"]
D --> D3["Loading the skeleton screen"]
E --> E1["mount/unmount Animation"]
E --> E2["Drag / Gestures"]
E --> E3["List layout Animation"]
E --> E4["Spring Physical Animation Effects"]
style C fill:#e3f2fd,stroke:#1565c0
style D fill:#e8f5e9,stroke:#2e7d32
style E fill:#fff3e0,stroke:#e65100
3. سيناريو واقعي
تتضمن لوحة التحكم الخاصة بـ«توم» ثلاثة تفاعلات أساسية: تحميل قائمة المهام، وتبديل النوافذ المنبثقة، وسحب البطاقات وفرزها. هدفه هو:
- عند تحميل قائمة المهام، تنزلق كل مهمة من اليسار.
- عند فتح النافذة المنبثقة، تظهر الخلفية تدريجيًّا وتقوم النافذة المنبثقة بالتكبير؛ وعند إغلاقها، يتم تشغيل الحركة في الاتجاه المعاكس.
- تتبع البطاقة إصبعك عند سحبها، وتعود إلى مكانها فور تركها.
بدأ بأبسط انتقالات CSS ثم انتقل تدريجيًا إلى إنشاء رسوم متحركة معقدة باستخدام Framer Motion.
(1) انتقالات CSS — أبسط أشكال التحسين التدريجي
تُستخدم انتقالات CSS لإنشاء انتقالات سلسة بين الحالة الأولية والحالة النهائية. وهي لا تتطلب أي مكتبات JavaScript، وتصبح سارية المفعول بمجرد الإعلان عنها في CSS.
الخصائص الفرعية الأربعة الأساسية للانتقال
/* transition: property duration timing-function delay */
.example {
transition: opacity 0.3s ease 0s;
/* Complete Syntax:
transition-property: opacity
transition-duration: 0.3s
transition-timing-function: ease
transition-delay: 0s
*/
}
القيم الشائعة لـ timing-function:
ease: بطيء → سريع → بطيء (الافتراضي)ease-in: بطيء → سرعة ثابتةease-out: سرعة ثابتة → بطيئةease-in-out: بطيء في كلا الطرفين، سريع في الوسطlinear: سرعة ثابتة طوال الوقتcubic-bezier(n, n, n, n): منحنيات بيزير المخصصة
▶ المثال 1: تنفيذ مكون «أكورديون» باستخدام انتقالات CSS
import { useState } from 'react'
const accordionData = [
{ title: 'What is React?', content: 'React It is a tool for building user interfaces. JavaScript library.' },
{ title: 'What Is a Component??', content: 'A component is React The smallest independent unit of an application,Reusable and combinable。' },
{ title: 'What is State?', content: 'State It is variable data within the component,Driver UI Update。' },
]
function Accordion() {
const [openIndex, setOpenIndex] = useState(null)
function toggle(index) {
setOpenIndex(openIndex === index ? null : index)
}
return (
<div style={{ maxWidth: 500, fontFamily: 'sans-serif' }}>
{accordionData.map((item, index) => (
<div
key={index}
style={{
border: '1px solid #e8e8e8',
borderRadius: 8,
marginBottom: 8,
overflow: 'hidden',
}}
>
<button
onClick={() => toggle(index)}
style={{
width: '100%',
padding: '12px 16px',
background: openIndex === index ? '#e6f7ff' : '#fafafa',
border: 'none',
cursor: 'pointer',
fontSize: 15,
fontWeight: 600,
textAlign: 'left',
transition: 'background 0.2s ease',
}}
>
{item.title}
</button>
<div
style={{
maxHeight: openIndex === index ? 80 : 0,
padding: openIndex === index ? '12px 16px' : '0 16px',
opacity: openIndex === index ? 1 : 0,
transition: 'all 0.3s ease',
color: '#666',
fontSize: 14,
lineHeight: 1.6,
}}
>
{item.content}
</div>
</div>
))}
</div>
)
}
النقطة الأساسية: يتم الانتقال إلى maxHeight + opacity + padding في وقت واحد لتحقيق تأثير توسيع/طي منطقة المحتوى. تجدر الإشارة إلى أن قيمة maxHeight يجب أن تكون أكبر قليلاً من الارتفاع الفعلي للمحتوى.
(2) الرسوم المتحركة في CSS — الرسوم المتحركة القائمة على الإطارات الرئيسية
عندما تحتوي الرسوم المتحركة على أكثر من مرحلتين (بخلاف مجرد «البداية → النهاية») أو تحتاج إلى التكرار، فإن «CSS Animation» هي الخيار الأفضل. فهي تستخدم @keyframes لتعريف تسلسلات الرسوم المتحركة متعددة المراحل.
السمات الأساسية
/* animation: name duration timing-function delay iteration-count direction fill-mode */
.pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
▶ المثال 2: الرسوم المتحركة لتحميل شاشة الهيكل العظمي
import './Skeleton.css'
function Skeleton({ count = 3 }) {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{Array.from({ length: count }).map((_, i) => (
<div key={i} className="skeleton-row">
<div className="skeleton-avatar" />
<div style={{ flex: 1 }}>
<div className="skeleton-line" style={{ width: '60%' }} />
<div className="skeleton-line" style={{ width: '90%', marginTop: 8 }} />
</div>
</div>
))}
</div>
)
}
export default Skeleton
/* Skeleton.css */
.skeleton-row {
display: flex;
gap: 12px;
align-items: center;
}
.skeleton-avatar {
width: 44px;
height: 44px;
border-radius: 50%;
background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
.skeleton-line {
height: 14px;
border-radius: 4px;
background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
كيف تعمل الرسوم المتحركة لشاشة الهيكل العظمي: تحاكي حركة background-position التأثير البصري لخط ضوئي يمر عبر الشاشة أثناء التحميل. وتؤدي الخاصية infinite في CSS Animation إلى تكرار عرض الخط الضوئي بشكل مستمر حتى ينتهي تحميل المحتوى الفعلي ويحل محل شاشة الهيكل العظمي.
(3) حركة الإطارات — الرسوم المتحركة المعقدة في React
تُعد «Framer Motion» أقوى مكتبة للرسوم المتحركة في منظومة React، وتقدم ثلاث إمكانيات أساسية:
| الإمكانية | المشكلة التي تم حلها | واجهة برمجة التطبيقات (API) المقابلة |
|---|---|---|
| الرسوم المتحركة للدخول/الخروج | الرسوم المتحركة عند تركيب/فك تركيب المكونات | motion.div + AnimatePresence |
| الرسوم المتحركة للتخطيط | انتقالات سلسة عند تغيير ترتيب القائمة | خاصية layout |
| الرسوم المتحركة للإيماءات | إيماءات السحب، التمرير فوق، والنقر | drag / whileHover / whileTap |
التثبيت
npm install framer-motion
▶ المثال 3: تنفيذ قائمة مهام باستخدام Framer Motion
import { useState } from 'react'
import { motion, AnimatePresence, Reorder } from 'framer-motion'
const initialTodos = [
{ id: 1, text: 'Done React Learning Animation', done: false },
{ id: 2, text: 'Reworking the Dashboard Components', done: false },
{ id: 3, text: 'Code Review PR #142', done: false },
{ id: 4, text: 'Update Project Documentation', done: true },
{ id: 5, text: 'Fix the Responsive Navigation BarBUG', done: false },
]
function TodoList() {
const [todos, setTodos] = useState(initialTodos)
function toggleDone(id) {
setTodos(prev =>
prev.map(t => (t.id === id ? { ...t, done: !t.done } : t))
)
}
function removeTodo(id) {
setTodos(prev => prev.filter(t => t.id !== id))
}
return (
<div style={{ maxWidth: 480, fontFamily: 'sans-serif' }}>
<AnimatePresence>
{todos.map(todo => (
<motion.div
key={todo.id}
layout
initial={{ opacity: 0, x: -60 }}
animate={{
opacity: 1,
x: 0,
background: todo.done ? '#f6ffed' : '#fff',
}}
exit={{ opacity: 0, x: 100, height: 0, marginBottom: 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '12px 16px',
marginBottom: 8,
borderRadius: 8,
border: '1px solid #f0f0f0',
cursor: 'pointer',
}}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<input
type="checkbox"
checked={todo.done}
onChange={() => toggleDone(todo.id)}
style={{ width: 18, height: 18, cursor: 'pointer' }}
/>
<span
style={{
textDecoration: todo.done ? 'line-through' : 'none',
color: todo.done ? '#999' : '#333',
fontSize: 15,
}}
>
{todo.text}
</span>
</div>
<button
onClick={() => removeTodo(todo.id)}
style={{
border: 'none',
background: 'none',
color: '#ff4d4f',
cursor: 'pointer',
fontSize: 18,
fontWeight: 'bold',
}}
>
x
</button>
</motion.div>
))}
</AnimatePresence>
</div>
)
}
export default TodoList
يوضح هذا الكود أربع ميزات رئيسية لـ Framer Motion:
initial/animate/exit: تحدد حالات الدخول والعرض والخروج للمكون. كما تتيحAnimatePresenceإمكانية تشغيل الرسوم المتحركة الخاصة بالخروج من خلال العرض الشرطي.layoutprop: عندما يتغير موضع عنصر في القائمة نتيجة لتغير في حالةdone، فإنه ينتقل تلقائيًا وبسلاسة إلى الموضع الجديد دون الحاجة إلى حساب الإحداثيات.transition: يستخدم الرسوم المتحركة الفيزيائية للزنبركspring؛stiffnessيتحكم في صلابة الزنبرك؛dampingيتحكم في التخميد (كلما ارتفعت القيمة، قلت مرونة الزنبرك)whileHover/whileTap: رسوم متحركة تفاعلية تعمل بالإيماءات؛ يتم تكبيرها إلى 1.02x عند تمرير الماوس فوقها، ويتم تصغيرها إلى 0.98x عند النقر عليها
▶ المثال 4: الرسوم المتحركة للنافذة المنبثقة «Framer Motion Modal»
import { useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
key="overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
style={{
position: 'fixed',
inset: 0,
background: 'rgba(0,0,0,0.45)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000,
}}
onClick={onClose}
>
<motion.div
key="modal"
initial={{ scale: 0.85, opacity: 0, y: 20 }}
animate={{ scale: 1, opacity: 1, y: 0 }}
exit={{ scale: 0.85, opacity: 0, y: 20 }}
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
style={{
background: '#fff',
padding: 24,
borderRadius: 12,
minWidth: 320,
boxShadow: '0 8px 32px rgba(0,0,0,0.12)',
position: 'relative',
}}
onClick={e => e.stopPropagation()}
>
{children}
</motion.div>
</motion.div>
)}
</AnimatePresence>
)
}
function App() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button
onClick={() => setIsOpen(true)}
style={{ padding: '8px 20px', fontSize: 15, cursor: 'pointer' }}
>
Open the pop-up window
</button>
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)}>
<h2 style={{ margin: '0 0 12px' }}>Confirm Action</h2>
<p style={{ color: '#666', marginBottom: 20 }}>
Are you sure you want to delete this record??This action cannot be undone.。
</p>
<div style={{ display: 'flex', gap: 12, justifyContent: 'flex-end' }}>
<button
onClick={() => setIsOpen(false)}
style={{ padding: '8px 20px', cursor: 'pointer' }}
>
Cancel
</button>
<button
onClick={() => {
alert('Deleted')
setIsOpen(false)
}}
style={{
padding: '8px 20px',
background: '#ff4d4f',
color: '#fff',
border: 'none',
borderRadius: 6,
cursor: 'pointer',
}}
>
Confirm Deletion
</button>
</div>
</Modal>
</div>
)
}
النقاط الرئيسية لتصميم الرسوم المتحركة المنبثقة:
- تستخدم كل من النوافذ المتراكبة والنوافذ المنبثقة مجموعة مختلفة من المؤثرات الحركية، لكن يتم إدارتها بشكل موحد باستخدام
AnimatePresence - استخدم الرسوم المتحركة
springللنوافذ المنبثقة، وstiffness: 400لجعلها تستجيب بسرعة، وdamping: 30لتقليل الارتداد - عند الظهور: تظهر النافذة المنبثقة بمقياس 0.85x + 20 بكسل أسفل
- عند الخروج: تكبير عكسي + تحرك لأسفل، على غرار حركة الدخول
4. أفضل الممارسات لتحسين أداء الرسوم المتحركة
| السمة | مرحلة التشغيل | الأداء | التوصية |
|---|---|---|---|
transform |
مركب | ✅ الأفضل | ⭐⭐⭐ |
opacity |
مركب | ✅ الأفضل | ⭐⭐⭐ |
filter |
لوحة | ⚠️ متوسطة | ⭐⭐ |
box-shadow |
لوحة | ⚠️ متوسطة | ⭐ |
width/height |
التصميم | ❌ باهظ الثمن | ❌ |
top/left/margin |
التصميم | ❌ باهظ الثمن | ❌ |
| القاعدة | الوصف |
|---|---|
استخدام transform وopacity بشكل افتراضي |
تعمل هاتان الخاصيتان على تشغيل مؤشر ترابط المُركِّب دون التسبب في عملية تخطيط/رسم |
تجنب استخدام الرسوم المتحركة width / height / top / left |
تؤدي هذه الخصائص إلى إعادة ترتيب التخطيط، وهو ما يستهلك موارد كثيرة |
استخدم will-change لإظهار رسالة في المتصفح |
will-change: transform, opacity لإنشاء طبقة مركبة مسبقًا |
| تجنب تحريك عدد كبير جدًا من العناصر في آن واحد | قد يؤدي تحريك أكثر من 20 عنصرًا في آن واحد إلى انخفاض معدل الإطارات |
استخدام requestAnimationFrame |
تجنب استخدام setTimeout في الرسوم المتحركة التي تعمل بواسطة جافا سكريبت |
▶ المثال 5: الرسوم المتحركة باستخدام الإطارات الرئيسية في CSS — شاشة الهيكل العظمي أثناء التحميل
function SkeletonCard() {
return (
<div style={{ border: '1px solid #f0f0f0', borderRadius: 8, padding: 16, overflow: 'hidden' }}>
<div style={{
height: 120, borderRadius: 4, marginBottom: 12,
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite',
}} />
<div style={{
height: 16, width: '60%', borderRadius: 4, marginBottom: 8,
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite',
}} />
<div style={{
height: 12, width: '40%', borderRadius: 4,
background: 'linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%)',
backgroundSize: '200% 100%',
animation: 'shimmer 1.5s infinite',
}} />
<style>{`
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
`}</style>
</div>
)
}
function LoadingGrid() {
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, maxWidth: 800, margin: '0 auto' }}>
{[1, 2, 3].map(i => <SkeletonCard key={i} />)}
</div>
)
}
❓ أسئلة شائعة
AnimatePresence في Framer Motion أحيانًا؟key لم يتم إعداده بشكل صحيح. تقوم AnimatePresence بتتبع العناصر الفرعية عبر key؛ فإذا بقيت المفتاح دون تغيير، فإنها تفسر العنصر على أنه «مُحدَّث» بدلاً من «مُحذوف»، ولن يتم تشغيل الرسوم المتحركة للخروج. بالإضافة إلى ذلك، يجب أن تكون العناصر الفرعية المباشرة لـ AnimatePresence مكونات motion.transform وopacity فقط، حيث تعمل هاتان الخاصيتان على طبقة التركيب الخاصة بوحدة معالجة الرسومات (GPU) في الأجهزة المحمولة. يستخدم Framer Motion transform بشكل افتراضي، مما يضمن أداءً جيدًا. إذا كان لا بد من إنشاء رسوم متحركة معقدة على الأجهزة المحمولة، فيمكنك استخدام ربط useWillChange أو ضبط will-change يدويًّا.layout بالاقتران مع layoutId وAnimatePresence لتحقيق انتقال مستقر للتخطيط. إذا كنت تستخدم CSS، ففكر في استخدام transform: translateX() بدلاً من left لرسوم الحركة المتعلقة بالموضع.useTransition في React 18 و Framer Motion؟useTransition هي ميزة متزامنة مدمجة في React تعمل على تصنيف تحديثات معينة للحالة على أنها «غير عاجلة» (مثل تصفية نتائج البحث)، مما يسمح بمعالجة التحديثات العاجلة (مثل الكتابة في حقل الإدخال) أولاً لمنع حدوث تقطع في العرض. أما Framer Motion فهي مكتبة رسوم متحركة تتولى معالجة التأثيرات المرئية مثل الحركة وتغيير الحجم وشفافية عناصر واجهة المستخدم. تعالج كل منهما مشكلات مختلفة: useTransition تعالج «تأخر التفاعل»، بينما تعالج Framer Motion «الانتقالات المرئية».📖 ملخص
- تُعد انتقالات CSS مناسبة للانتقالات البسيطة المكونة من مرحلتين والتي يتم تشغيلها تلقائيًا عند تغير الحالة.
- يمكن لـ «الرسوم المتحركة في CSS»، عند استخدامها مع
@keyframes، إنشاء رسوم متحركة معقدة ومتعددة المراحل ومتكررة. - تغطي مكونات
motionوAnimatePresenceفي Framer Motion جميع سيناريوهات الرسوم المتحركة لمكونات React - تتحكم الخصائص الثلاث
initialوanimateوexitفي حالات الدخول والعرض والخروج، على التوالي. layoutيتيحpropانتقالات سلسة عند إعادة ترتيب القوائم، دون الحاجة إلى حساب الإحداثيات يدويًّا- المبادئ الأساسية لأداء الرسوم المتحركة: أعطِ الأولوية دائمًا لـ
transformوopacity
📝 تمارين
- استخدم انتقالات CSS لتنفيذ زر «العودة إلى الأعلى»: يظهر الزر تدريجيًا عند التمرير لأكثر من 300 بكسل؛ وعند النقر عليه، يتم التمرير بسلاسة إلى الأعلى؛ ثم يتلاشى ويختفي بمجرد الوصول إلى الأعلى.
- استخدم الرسوم المتحركة في CSS لإنشاء مؤشر التحميل: رسوم متحركة على شكل دائرة دوارة. استخدم
@keyframesللتحكم في الدوران من 0 درجة إلى 360 درجة، واستخدمlinearلمنحنى السرعة للحفاظ على سرعة ثابتة. - استخدم Framer Motion لإنشاء زر إجراء عائم (FAB) قابل للسحب. تتيح الخاصية
dragسحبه إلى أي مكان على الشاشة، وعند انتهاء السحب، استخدم الرسوم المتحركةspringلتثبيته عند أقرب حافة للشاشة.