React: الانتقالات والرسوم المتحركة

آخر تحديث: 2026-08-26

كان توم قد انتهى لتوه من مشروع لوحة بيانات، لكن مدير المنتج أشار إلى أن انتقالات الصفحات كانت مفاجئة للغاية — حيث كانت البيانات تظهر مصحوبة بـ«وميض» عند تحميل القائمة، وكانت النوافذ المنبثقة تفتح وتغلق على الفور. أدرك توم أن توفر جميع الميزات لا يعني بالضرورة تجربة مستخدم جيدة. كان عليه إضافة رسوم متحركة انتقالية سلسة إلى الصفحات.


1. ما ستتعلمه



2. الرسوم التخطيطية المفاهيمية

سيساعدك الرسم التوضيحي التالي على اتخاذ قرارات سريعة عند اختيار خيار الرسوم المتحركة:

100%
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. سيناريو واقعي

تتضمن لوحة التحكم الخاصة بـ«توم» ثلاثة تفاعلات أساسية: تحميل قائمة المهام، وتبديل النوافذ المنبثقة، وسحب البطاقات وفرزها. هدفه هو:

  1. عند تحميل قائمة المهام، تنزلق كل مهمة من اليسار.
  2. عند فتح النافذة المنبثقة، تظهر الخلفية تدريجيًّا وتقوم النافذة المنبثقة بالتكبير؛ وعند إغلاقها، يتم تشغيل الحركة في الاتجاه المعاكس.
  3. تتبع البطاقة إصبعك عند سحبها، وتعود إلى مكانها فور تركها.

بدأ بأبسط انتقالات CSS ثم انتقل تدريجيًا إلى إنشاء رسوم متحركة معقدة باستخدام Framer Motion.


(1) انتقالات CSS — أبسط أشكال التحسين التدريجي

تُستخدم انتقالات CSS لإنشاء انتقالات سلسة بين الحالة الأولية والحالة النهائية. وهي لا تتطلب أي مكتبات JavaScript، وتصبح سارية المفعول بمجرد الإعلان عنها في CSS.

الخصائص الفرعية الأربعة الأساسية للانتقال

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:

▶ المثال 1: تنفيذ مكون «أكورديون» باستخدام انتقالات CSS

JSX
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 لتعريف تسلسلات الرسوم المتحركة متعددة المراحل.

السمات الأساسية

CSS
/* 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: الرسوم المتحركة لتحميل شاشة الهيكل العظمي

JSX
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
▶ جرّب الكود
CSS
/* 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

التثبيت

BASH
npm install framer-motion

▶ المثال 3: تنفيذ قائمة مهام باستخدام Framer Motion

JSX
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:

  1. initial / animate / exit: تحدد حالات الدخول والعرض والخروج للمكون. كما تتيح AnimatePresence إمكانية تشغيل الرسوم المتحركة الخاصة بالخروج من خلال العرض الشرطي.
  2. layout prop: عندما يتغير موضع عنصر في القائمة نتيجة لتغير في حالة done، فإنه ينتقل تلقائيًا وبسلاسة إلى الموضع الجديد دون الحاجة إلى حساب الإحداثيات.
  3. transition: يستخدم الرسوم المتحركة الفيزيائية للزنبرك spring؛ stiffness يتحكم في صلابة الزنبرك؛ damping يتحكم في التخميد (كلما ارتفعت القيمة، قلت مرونة الزنبرك)
  4. whileHover / whileTap: رسوم متحركة تفاعلية تعمل بالإيماءات؛ يتم تكبيرها إلى 1.02x عند تمرير الماوس فوقها، ويتم تصغيرها إلى 0.98x عند النقر عليها

▶ المثال 4: الرسوم المتحركة للنافذة المنبثقة «Framer Motion Modal»

JSX
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>
  )
}

النقاط الرئيسية لتصميم الرسوم المتحركة المنبثقة:



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 — شاشة الهيكل العظمي أثناء التحميل

JSX
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>
  )
}
▶ جرّب الكود

❓ أسئلة شائعة

س ما الفرق بالضبط بين انتقالات CSS والرسوم المتحركة في CSS؟
ج الانتقال هو عملية انتقال من مرحلتين «من أ إلى ب» يتم تشغيلها بفعل تغيير في الحالة (مثل تمرير المؤشر فوق العنصر)، ولا يدعم التكرار. أما الرسوم المتحركة فهي رسوم متحركة متعددة المراحل تعتمد على الإطارات الرئيسية، ويمكن تكرارها إلى ما لا نهاية، وتشغيلها عكسيًّا، وإيقافها مؤقتًا أو استئنافها. استخدم الانتقالات في الحالات البسيطة، والرسوم المتحركة في التسلسلات المعقدة.
س لماذا لا يعمل AnimatePresence في Framer Motion أحيانًا؟
ج السبب الأكثر شيوعًا هو أن key لم يتم إعداده بشكل صحيح. تقوم AnimatePresence بتتبع العناصر الفرعية عبر key؛ فإذا بقيت المفتاح دون تغيير، فإنها تفسر العنصر على أنه «مُحدَّث» بدلاً من «مُحذوف»، ولن يتم تشغيل الرسوم المتحركة للخروج. بالإضافة إلى ذلك، يجب أن تكون العناصر الفرعية المباشرة لـ AnimatePresence مكونات motion.
س كيف يمكن ضمان أداء الرسوم المتحركة على الأجهزة المحمولة؟
ج احرص على استخدام الرسوم المتحركة transform وopacity فقط، حيث تعمل هاتان الخاصيتان على طبقة التركيب الخاصة بوحدة معالجة الرسومات (GPU) في الأجهزة المحمولة. يستخدم Framer Motion transform بشكل افتراضي، مما يضمن أداءً جيدًا. إذا كان لا بد من إنشاء رسوم متحركة معقدة على الأجهزة المحمولة، فيمكنك استخدام ربط useWillChange أو ضبط will-change يدويًّا.
س ماذا أفعل إذا تسببت الرسوم المتحركة في حدوث تغييرات في التخطيط؟
ج خصص حجمًا ثابتًا للعناصر المتحركة لمنع تغير التخطيط أثناء عرض الرسوم المتحركة. في Framer Motion، يمكن استخدام الخاصية layout بالاقتران مع layoutId وAnimatePresence لتحقيق انتقال مستقر للتخطيط. إذا كنت تستخدم CSS، ففكر في استخدام transform: translateX() بدلاً من left لرسوم الحركة المتعلقة بالموضع.
س ما الفرق بين ميزة useTransition في React 18 و Framer Motion؟
ج useTransition هي ميزة متزامنة مدمجة في React تعمل على تصنيف تحديثات معينة للحالة على أنها «غير عاجلة» (مثل تصفية نتائج البحث)، مما يسمح بمعالجة التحديثات العاجلة (مثل الكتابة في حقل الإدخال) أولاً لمنع حدوث تقطع في العرض. أما Framer Motion فهي مكتبة رسوم متحركة تتولى معالجة التأثيرات المرئية مثل الحركة وتغيير الحجم وشفافية عناصر واجهة المستخدم. تعالج كل منهما مشكلات مختلفة: useTransition تعالج «تأخر التفاعل»، بينما تعالج Framer Motion «الانتقالات المرئية».

📖 ملخص


📝 تمارين

  1. استخدم انتقالات CSS لتنفيذ زر «العودة إلى الأعلى»: يظهر الزر تدريجيًا عند التمرير لأكثر من 300 بكسل؛ وعند النقر عليه، يتم التمرير بسلاسة إلى الأعلى؛ ثم يتلاشى ويختفي بمجرد الوصول إلى الأعلى.
  2. استخدم الرسوم المتحركة في CSS لإنشاء مؤشر التحميل: رسوم متحركة على شكل دائرة دوارة. استخدم @keyframes للتحكم في الدوران من 0 درجة إلى 360 درجة، واستخدم linear لمنحنى السرعة للحفاظ على سرعة ثابتة.
  3. استخدم Framer Motion لإنشاء زر إجراء عائم (FAB) قابل للسحب. تتيح الخاصية drag سحبه إلى أي مكان على الشاشة، وعند انتهاء السحب، استخدم الرسوم المتحركة spring لتثبيته عند أقرب حافة للشاشة.
Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%