CSS3 والتصميم المتجاوب

حركات الإطارات المفتاحية في CSS

35 دقيقة الدرس 41 من 60

ما هي حركات الإطارات المفتاحية في CSS؟

تتيح لك حركات الإطارات المفتاحية في CSS إنشاء حركات معقدة ومتعددة الخطوات بالكامل في CSS دون أي JavaScript. بينما تتيح لك انتقالات CSS التحريك بين حالتين (بداية ونهاية)، تتيح لك حركات الإطارات المفتاحية تحديد أي عدد تريده من الخطوات الوسيطة. يمكنك جعل عنصر يدور، يرتد، ينبض، يتلاشى عبر ألوان متعددة، أو يتبع مسار حركة معقد -- كل ذلك بوصف إطارات مفتاحية يقوم المتصفح بالتداخل بينها بسلاسة.

تتكون حركات الإطارات المفتاحية من جزأين: قاعدة @keyframes التي تحدد تسلسل الحركة، وخصائص الحركة التي تطبق ذلك التسلسل على عنصر. هذا الفصل بين التعريف والتطبيق يعني أنه يمكنك إعادة استخدام نفس الحركة على عناصر متعددة بمدد وتأخيرات ودوال توقيت مختلفة. بمجرد فهم حركات الإطارات المفتاحية، يمكنك بناء مؤشرات تحميل دوارة، وتأثيرات واجهة مستخدم ملفتة للانتباه، وحركات دخول الصفحة، وأكثر من ذلك بكثير -- كل ذلك بـ CSS فقط.

قاعدة @keyframes

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

تسمية الإطارات المفتاحية

كل قاعدة @keyframes تحتاج إلى اسم. هذا الاسم هو معرف مخصص ستشير إليه لاحقا عند تطبيق الحركة على عنصر. يتبع الاسم نفس قواعد المعرفات المخصصة في CSS: يمكن أن يحتوي على حروف وأرقام وشرطات وشرطات سفلية، لكن لا يمكن أن يبدأ برقم. اختر أسماء وصفية توضح ما تفعله الحركة.

تسمية حركات الإطارات المفتاحية

/* جيد: أسماء وصفية */
@keyframes fadeIn { /* ... */ }
@keyframes slideUpFromBottom { /* ... */ }
@keyframes pulseGlow { /* ... */ }
@keyframes spinClockwise { /* ... */ }
@keyframes bounceIn { /* ... */ }

/* تجنب: أسماء غامضة أو مربكة */
@keyframes anim1 { /* ... */ }
@keyframes myAnimation { /* ... */ }

/* صالح لكن غير مستحسن: أسماء تطابق كلمات CSS المحجوزة */
/* تجنب أسماء مثل "none"، "inherit"، "initial"، "unset" */
ملاحظة: أسماء الإطارات المفتاحية حساسة لحالة الأحرف. fadeIn وfadein يُعاملان كحركتين مختلفتين. أيضا تجنب تسمية إطاراتك المفتاحية بقيم CSS العامة مثل none وinitial وinherit أو unset، لأنها ستُفسر ككلمات مفتاحية بدلا من أسماء حركات.

صيغة from/to

أبسط طريقة لتحديد الإطارات المفتاحية هي استخدام الكلمتين from وto. تمثل from بداية الحركة (0%) وto تمثل النهاية (100%). هذا مثالي للحركات البسيطة ذات الحالتين.

استخدام from/to

/* ظهور تدريجي بسيط */
@keyframes fadeIn {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

/* انزلاق من اليسار */
@keyframes slideInLeft {
    from {
        transform: translateX(-100%);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

/* دوران العنصر */
@keyframes rotate360 {
    from {
        transform: rotate(0deg);
    }
    to {
        transform: rotate(360deg);
    }
}

الإطارات المفتاحية بالنسب المئوية

للحركات التي تحتوي على أكثر من حالتين، تستخدم النسب المئوية لتحديد الإطارات المفتاحية في أي نقطة خلال الجدول الزمني للحركة. تمنحك النسب المئوية تحكما دقيقا في ما يحدث في كل مرحلة. يمكنك تحديد أي عدد من خطوات الإطارات المفتاحية، من 0% إلى 100%.

حركات متعددة المراحل بالنسب المئوية

/* تأثير الارتداد بمراحل متعددة */
@keyframes bounce {
    0% {
        transform: translateY(0);
    }
    25% {
        transform: translateY(-30px);
    }
    50% {
        transform: translateY(0);
    }
    75% {
        transform: translateY(-15px);
    }
    100% {
        transform: translateY(0);
    }
}

/* دورة ألوان عبر ألوان متعددة */
@keyframes colorCycle {
    0% { background-color: #e74c3c; }
    25% { background-color: #f39c12; }
    50% { background-color: #2ecc71; }
    75% { background-color: #3498db; }
    100% { background-color: #e74c3c; }
}

/* حركة دخول درامية */
@keyframes dramaticEntrance {
    0% {
        opacity: 0;
        transform: scale(0.3) rotate(-15deg);
    }
    50% {
        opacity: 0.8;
        transform: scale(1.05) rotate(3deg);
    }
    70% {
        transform: scale(0.95) rotate(-1deg);
    }
    100% {
        opacity: 1;
        transform: scale(1) rotate(0deg);
    }
}

يمكنك أيضا دمج محددات نسب مئوية متعددة تشترك في نفس الأنماط، باستخدام قائمة مفصولة بفواصل:

دمج محددات الإطارات المفتاحية

/* حركة وميض: مرئي في البداية والمنتصف والنهاية */
@keyframes blink {
    0%, 50%, 100% {
        opacity: 1;
    }
    25%, 75% {
        opacity: 0;
    }
}

/* الاحتفاظ بحالة لجزء من الحركة */
@keyframes fadeInAndHold {
    0% {
        opacity: 0;
    }
    30% {
        opacity: 1;
    }
    /* الاحتفاظ بالشفافية الكاملة من 30% إلى 100% */
    100% {
        opacity: 1;
    }
}

خصائص الحركة بالتفصيل

بمجرد تحديد قاعدة @keyframes، تطبقها على عنصر باستخدام خصائص الحركة. هناك ثماني خصائص فردية تتحكم في كل جانب من جوانب سلوك الحركة.

animation-name

تحدد خاصية animation-name قاعدة @keyframes المراد تطبيقها. يجب أن تطابق الاسم الذي استخدمته في تصريح @keyframes بالضبط، بما في ذلك حالة الأحرف. اضبطها على none لإزالة الحركة.

تطبيق حركة بالاسم

.element {
    animation-name: fadeIn;
}

/* إزالة حركة */
.element.paused {
    animation-name: none;
}

animation-duration

تحدد خاصية animation-duration المدة التي تستغرقها دورة واحدة من الحركة للاكتمال. تقبل قيم زمنية بالثواني (s) أو بالمللي ثانية (ms). القيمة الافتراضية هي 0s، مما يعني عدم ظهور أي حركة -- يجب عليك دائما تعيين مدة لأي حركة لتشغيلها.

تعيين مدة الحركة

.fast-animation {
    animation-name: fadeIn;
    animation-duration: 0.3s;
}

.normal-animation {
    animation-name: fadeIn;
    animation-duration: 1s;
}

.slow-animation {
    animation-name: fadeIn;
    animation-duration: 3s;
}

/* المللي ثانية تعمل أيضا */
.precise-animation {
    animation-name: fadeIn;
    animation-duration: 750ms;
}

animation-timing-function

تتحكم animation-timing-function في منحنى التسارع للحركة. تحدد كيف تتقدم الحركة بمرور الوقت -- سواء بدأت ببطء وتسارعت، أو حافظت على سرعة ثابتة، أو اتبعت منحنى مخصصا. هذه من أهم الخصائص لجعل الحركات تبدو طبيعية.

دوال التوقيت للحركات

/* قيم الكلمات المفتاحية المدمجة */
.linear { animation-timing-function: linear; }
/* سرعة ثابتة من البداية إلى النهاية */

.ease { animation-timing-function: ease; }
/* بداية بطيئة، منتصف سريع، نهاية بطيئة (الافتراضي) */

.ease-in { animation-timing-function: ease-in; }
/* بداية بطيئة، تتسارع نحو النهاية */

.ease-out { animation-timing-function: ease-out; }
/* بداية سريعة، تتباطأ نحو النهاية */

.ease-in-out { animation-timing-function: ease-in-out; }
/* بداية بطيئة ونهاية بطيئة */

/* منحنيات cubic-bezier مخصصة */
.custom-bounce {
    animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
}

/* خطوات للحركة إطارا بإطار */
.sprite-animation {
    animation-timing-function: steps(8);
    /* القفز عبر 8 خطوات منفصلة */
}

.typewriter {
    animation-timing-function: steps(20, end);
    /* 20 خطوة، القفز في نهاية كل خطوة */
}
نصيحة: يمكنك تعيين دوال توقيت مختلفة لمقاطع إطارات مفتاحية فردية بوضع animation-timing-function داخل كتلة إطار مفتاحي. دالة التوقيت المعلنة عند إطار مفتاحي تتحكم في التسارع من ذلك الإطار المفتاحي إلى التالي، مما يمنحك تحكما دقيقا في كل مرحلة من الحركة.

دوال توقيت لكل إطار مفتاحي

@keyframes customBounce {
    0% {
        transform: translateY(0);
        animation-timing-function: ease-out;
        /* تباطؤ من 0% إلى 40% */
    }
    40% {
        transform: translateY(-150px);
        animation-timing-function: ease-in;
        /* تسارع من 40% إلى 60% */
    }
    60% {
        transform: translateY(0);
        animation-timing-function: ease-out;
        /* تباطؤ من 60% إلى 80% */
    }
    80% {
        transform: translateY(-40px);
        animation-timing-function: ease-in;
    }
    100% {
        transform: translateY(0);
    }
}

animation-delay

تحدد خاصية animation-delay مدة الانتظار قبل بدء الحركة. تقبل قيم زمنية بالثواني أو المللي ثانية. القيمة الموجبة تؤخر البدء، بينما القيمة السالبة تجعل الحركة تبدأ فورا لكن في منتصف دورتها، كما لو كانت تعمل بالفعل لتلك المدة من الوقت.

تأخير الحركة: موجب وسالب

/* الانتظار 0.5 ثانية قبل البدء */
.delayed {
    animation-name: fadeIn;
    animation-duration: 1s;
    animation-delay: 0.5s;
}

/* تأخير سالب: البدء فورا، لكن بعد 500 مللي ثانية في الحركة */
.already-running {
    animation-name: rotate360;
    animation-duration: 2s;
    animation-delay: -0.5s;
    /* الحركة تبدأ عند علامة 25% فورا */
}

/* توزيع عناصر متعددة بتأخيرات متزايدة */
.item:nth-child(1) { animation-delay: 0s; }
.item:nth-child(2) { animation-delay: 0.1s; }
.item:nth-child(3) { animation-delay: 0.2s; }
.item:nth-child(4) { animation-delay: 0.3s; }
.item:nth-child(5) { animation-delay: 0.4s; }
نصيحة: التأخيرات السالبة مفيدة للغاية لإنشاء حركات متداخلة حيث تريد أن تظهر العناصر وكأنها بالفعل في حالة حركة. على سبيل المثال، إذا كان لديك عناصر مؤشر دوار متعددة، يمكنك إعطاء كل منها تأخيرا سالبا مختلفا بحيث تبدأ في مواضع مختلفة في دورة الدوران، مما يخلق تأثيرا طبيعيا أكثر توزعا.

animation-iteration-count

تحدد خاصية animation-iteration-count عدد مرات تشغيل الحركة. تقبل أي رقم موجب (بما في ذلك الكسور العشرية) أو الكلمة المفتاحية infinite للتكرار المستمر.

التحكم في عدد التكرارات

/* التشغيل مرة واحدة (الافتراضي) */
.once { animation-iteration-count: 1; }

/* التشغيل ثلاث مرات */
.thrice { animation-iteration-count: 3; }

/* تشغيل نصف دورة (التوقف عند 50%) */
.half { animation-iteration-count: 0.5; }

/* التكرار للأبد */
.forever { animation-iteration-count: infinite; }

/* عملي: مؤشر تحميل دوار يعمل بلا نهاية */
.spinner {
    animation-name: rotate360;
    animation-duration: 1s;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
}

animation-direction

تتحكم خاصية animation-direction في ما إذا كانت الحركة تعمل للأمام أو للخلف أو تتناوب بين الاثنين في الدورات المتتالية. هذا قوي بشكل خاص عند دمجه مع تكرارات متعددة.

قيم اتجاه الحركة

/* normal: تشغيل من 0% إلى 100% في كل دورة (الافتراضي) */
.forward { animation-direction: normal; }

/* reverse: تشغيل من 100% إلى 0% في كل دورة */
.backward { animation-direction: reverse; }

/* alternate: الدورات الفردية للأمام، الدورات الزوجية للخلف */
.ping-pong {
    animation-direction: alternate;
    animation-iteration-count: infinite;
    /* الأولى: 0%→100%، الثانية: 100%→0%، الثالثة: 0%→100%، ... */
}

/* alternate-reverse: الدورات الفردية للخلف، الدورات الزوجية للأمام */
.reverse-ping-pong {
    animation-direction: alternate-reverse;
    animation-iteration-count: infinite;
    /* الأولى: 100%→0%، الثانية: 0%→100%، الثالثة: 100%→0%، ... */
}

/* عملي: عنصر نابض */
@keyframes pulse {
    from { transform: scale(1); }
    to { transform: scale(1.1); }
}

.pulse {
    animation-name: pulse;
    animation-duration: 0.8s;
    animation-direction: alternate;
    animation-iteration-count: infinite;
    animation-timing-function: ease-in-out;
}

animation-fill-mode

تحدد خاصية animation-fill-mode الأنماط التي تُطبق على العنصر قبل بدء الحركة (أثناء التأخير) وبعد انتهائها. بشكل افتراضي، يعود العنصر المتحرك إلى أنماطه الأصلية بمجرد انتهاء الحركة. يتيح لك وضع الملء تجاوز هذا السلوك.

فهم animation-fill-mode

/* none (الافتراضي): يعود العنصر لأنماطه الأصلية قبل/بعد */
.no-fill {
    animation-fill-mode: none;
    opacity: 1; /* الأصلي */
    animation-name: fadeIn; /* يحرك من opacity:0 إلى opacity:1 */
    /* قبل التأخير: الشفافية 1 (الأصلي) */
    /* بعد الحركة: الشفافية 1 (الأصلي) */
}

/* forwards: يحتفظ العنصر بأنماط الإطار المفتاحي الأخير */
.keep-end-state {
    animation-fill-mode: forwards;
    opacity: 1;
    animation-name: fadeOut; /* يحرك من opacity:1 إلى opacity:0 */
    /* بعد الحركة: الشفافية تبقى 0 (الإطار المفتاحي الأخير) */
}

/* backwards: يطبق العنصر أنماط الإطار المفتاحي الأول أثناء التأخير */
.apply-start-early {
    animation-fill-mode: backwards;
    opacity: 1;
    animation-name: fadeIn; /* يبدأ من opacity:0 */
    animation-delay: 2s;
    /* أثناء التأخير 2 ثانية: الشفافية 0 (الإطار المفتاحي الأول) */
    /* بعد الحركة: الشفافية 1 (الأصلي) */
}

/* both: يجمع بين سلوك forwards وbackwards */
.fill-both {
    animation-fill-mode: both;
    opacity: 1;
    animation-name: fadeIn;
    animation-delay: 2s;
    /* أثناء التأخير: الشفافية 0 (الإطار المفتاحي الأول - backwards) */
    /* بعد الحركة: الشفافية 1 (الإطار المفتاحي الأخير - forwards) */
}
تحذير: animation-fill-mode: forwards يبقي أنماط الإطار المفتاحي الأخير مطبقة إلى أجل غير مسمى. هذا قد يسبب ارتباكا أثناء تصحيح الأخطاء لأن الأنماط المحسوبة للعنصر ستظهر قيم الإطار المفتاحي بدلا من CSS الأصلي. إذا وجدت أن عنصرا لا يستجيب لتغييرات الأنماط كما هو متوقع، تحقق مما إذا كان وضع الملء يتجاوز أنماطك.

animation-play-state

تتيح لك خاصية animation-play-state إيقاف الحركة مؤقتا واستئنافها. تقبل قيمتين: running (الافتراضي) وpaused. غالبا ما يتم تبديلها عبر JavaScript أو أشباه فئات CSS مثل :hover.

إيقاف واستئناف الحركات

/* إيقاف الحركة عند التمرير */
.animated-element {
    animation: rotate360 2s linear infinite;
}

.animated-element:hover {
    animation-play-state: paused;
}

/* إيقاف عندما يحتوي العنصر الأب على فئة معينة */
.container.frozen .animated-element {
    animation-play-state: paused;
}

/* مفيد لتفضيلات تقليل الحركة */
@media (prefers-reduced-motion: reduce) {
    * {
        animation-play-state: paused !important;
    }
}

اختصار animation

تتيح لك خاصية الاختصار animation دمج جميع خصائص الحركة في تصريح واحد. الصيغة مرنة -- يتم تحديد القيم حسب النوع وليس الموضع، مع استثناء واحد: قيمة الوقت الأولى هي دائما animation-duration والثانية هي animation-delay.

صيغة اختصار الحركة

/* الصيغة:
   animation: name duration timing-function delay iteration-count
              direction fill-mode play-state; */

/* التصريح المطول الكامل */
.element {
    animation-name: fadeIn;
    animation-duration: 1s;
    animation-timing-function: ease-out;
    animation-delay: 0.5s;
    animation-iteration-count: 1;
    animation-direction: normal;
    animation-fill-mode: forwards;
    animation-play-state: running;
}

/* الاختصار المكافئ */
.element {
    animation: fadeIn 1s ease-out 0.5s 1 normal forwards running;
}

/* يمكنك حذف القيم التي تستخدم الافتراضيات */
.element {
    animation: fadeIn 1s ease-out 0.5s forwards;
}

/* الحد الأدنى المطلوب: الاسم والمدة */
.element {
    animation: fadeIn 1s;
}

/* مع التكرار اللانهائي */
.spinner {
    animation: rotate360 1s linear infinite;
}

/* مع الاتجاه المتناوب */
.pulse {
    animation: pulse 0.8s ease-in-out infinite alternate;
}

حركات متعددة

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

تطبيق حركات متعددة

@keyframes slideIn {
    from { transform: translateX(-100%); }
    to { transform: translateX(0); }
}

@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

@keyframes colorShift {
    from { color: #333; }
    to { color: #0066cc; }
}

/* تطبيق الثلاثة في وقت واحد */
.element {
    animation:
        slideIn 0.6s ease-out forwards,
        fadeIn 0.4s ease-out forwards,
        colorShift 1s ease-in-out 0.6s forwards;
}

/* كل حركة مستقلة:
   - slideIn: 0.6 ثانية مع ease-out
   - fadeIn: 0.4 ثانية مع ease-out
   - colorShift: تبدأ بعد تأخير 0.6 ثانية، تستمر 1 ثانية */

ربط الحركات بالتأخيرات

يمكنك إنشاء سلاسل حركات متتابعة باستخدام تأخيرات تطابق مدة الحركات السابقة. هذا يتيح لك بناء تسلسلات حركة خطوة بخطوة بدون JavaScript.

ربط الحركات بشكل متتابع

@keyframes slideDown {
    from { transform: translateY(-50px); opacity: 0; }
    to { transform: translateY(0); opacity: 1; }
}

@keyframes expand {
    from { width: 0; }
    to { width: 100%; }
}

@keyframes revealContent {
    from { opacity: 0; max-height: 0; }
    to { opacity: 1; max-height: 500px; }
}

/* السلسلة: slideDown ينتهي، ثم expand يبدأ، ثم revealContent */
.card-header {
    animation: slideDown 0.5s ease-out both;
}

.card-divider {
    animation: expand 0.3s ease-out 0.5s both;
    /* يبدأ بعد انتهاء slideDown (تأخير 0.5 ثانية) */
}

.card-body {
    animation: revealContent 0.4s ease-out 0.8s both;
    /* يبدأ بعد انتهاء expand (0.5 + 0.3 = 0.8 ثانية تأخير) */
}

أحداث الحركة في JavaScript

تطلق حركات CSS ثلاثة أحداث JavaScript تتيح لك الاستجابة لمراحل الحركة الرئيسية. هذه الأحداث ضرورية لتنسيق الحركات مع منطق التطبيق، أو تشغيل إجراءات متابعة، أو التنظيف بعد اكتمال الحركات.

الاستماع لأحداث الحركة

const element = document.querySelector('.animated');

/* يُطلق عند بدء الحركة (بعد أي تأخير) */
element.addEventListener('animationstart', (e) => {
    console.log('بدأت الحركة:', e.animationName);
    console.log('الوقت المنقضي:', e.elapsedTime);
});

/* يُطلق عند بداية كل تكرار جديد (ليس الأول) */
element.addEventListener('animationiteration', (e) => {
    console.log('تكرار جديد لـ:', e.animationName);
});

/* يُطلق عند اكتمال الحركة */
element.addEventListener('animationend', (e) => {
    console.log('انتهت الحركة:', e.animationName);
    /* نمط شائع: إزالة فئة الحركة */
    element.classList.remove('animate-in');
    /* أو تشغيل إجراء متابعة */
    showNextElement();
});

/* يُطلق إذا تم إلغاء الحركة (مثلا، إزالة العنصر) */
element.addEventListener('animationcancel', (e) => {
    console.log('تم إلغاء الحركة:', e.animationName);
});
ملاحظة: حدث animationiteration لا يُطلق في الدورة الأولى -- فقط في التكرارات اللاحقة. إذا كان لحركتك animation-iteration-count: 1، فلن يُطلق هذا الحدث أبدا. أيضا، إذا تمت إزالة الحركة أو إخفاء العنصر قبل الاكتمال، يُطلق حدث animationcancel بدلا من animationend.

أمثلة حركات عملية

دعنا نبني عدة حركات واقعية ستستخدمها بشكل متكرر في تطوير الويب. كل مثال يوضح تقنيات ومجموعات مختلفة من خصائص الحركة.

مؤشر التحميل الدوار

مؤشر تحميل دوار بـ CSS فقط

/* HTML: <div class="spinner"></div> */

@keyframes spin {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

.spinner {
    width: 40px;
    height: 40px;
    border: 4px solid #e0e0e0;
    border-top-color: #3498db;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

/* بديل: مؤشر حلقة مزدوجة */
@keyframes dualSpin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}

.dual-spinner {
    width: 48px;
    height: 48px;
    border: 5px solid transparent;
    border-top-color: #3498db;
    border-bottom-color: #3498db;
    border-radius: 50%;
    animation: dualSpin 1s linear infinite;
}

/* مؤشر تحميل ثلاث نقاط */
@keyframes dotPulse {
    0%, 80%, 100% {
        transform: scale(0);
        opacity: 0.5;
    }
    40% {
        transform: scale(1);
        opacity: 1;
    }
}

.dot-loader {
    display: flex;
    gap: 8px;
}

.dot-loader span {
    width: 12px;
    height: 12px;
    background: #3498db;
    border-radius: 50%;
    animation: dotPulse 1.4s ease-in-out infinite;
}

.dot-loader span:nth-child(2) {
    animation-delay: 0.16s;
}

.dot-loader span:nth-child(3) {
    animation-delay: 0.32s;
}

تأثير النبض

شارة إشعار نابضة

@keyframes pulse {
    0% {
        transform: scale(1);
        box-shadow: 0 0 0 0 rgba(231, 76, 60, 0.7);
    }
    50% {
        transform: scale(1.05);
        box-shadow: 0 0 0 10px rgba(231, 76, 60, 0);
    }
    100% {
        transform: scale(1);
        box-shadow: 0 0 0 0 rgba(231, 76, 60, 0);
    }
}

.notification-badge {
    width: 24px;
    height: 24px;
    background: #e74c3c;
    border-radius: 50%;
    color: white;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 12px;
    animation: pulse 2s ease-in-out infinite;
}

/* نبض خفيف لأزرار الدعوة للعمل */
@keyframes subtlePulse {
    0%, 100% {
        box-shadow: 0 0 0 0 rgba(52, 152, 219, 0.4);
    }
    50% {
        box-shadow: 0 0 0 15px rgba(52, 152, 219, 0);
    }
}

.cta-button {
    padding: 12px 32px;
    background: #3498db;
    color: white;
    border: none;
    border-radius: 6px;
    animation: subtlePulse 2.5s ease-in-out infinite;
}

.cta-button:hover {
    animation-play-state: paused;
}

تأثير الآلة الكاتبة

حركة الآلة الكاتبة بـ CSS

@keyframes typing {
    from { width: 0; }
    to { width: 100%; }
}

@keyframes blinkCaret {
    0%, 100% { border-right-color: transparent; }
    50% { border-right-color: #333; }
}

.typewriter {
    display: inline-block;
    overflow: hidden;
    white-space: nowrap;
    border-right: 3px solid #333;
    font-family: monospace;
    font-size: 1.25rem;
    /* عدد الخطوات يجب أن يطابق عدد أحرف النص */
    animation:
        typing 3s steps(30) 1s forwards,
        blinkCaret 0.75s step-end infinite;
    width: 0;
}

/* محتوى النص يحدد عدد الخطوات:
   "مرحبا بكم في موقعي الشخصي" = 30 حرفا = steps(30) */

دخول بالارتداد

حركة الدخول بالارتداد

@keyframes bounceIn {
    0% {
        opacity: 0;
        transform: scale(0.3);
    }
    20% {
        transform: scale(1.1);
    }
    40% {
        transform: scale(0.9);
    }
    60% {
        opacity: 1;
        transform: scale(1.03);
    }
    80% {
        transform: scale(0.97);
    }
    100% {
        opacity: 1;
        transform: scale(1);
    }
}

.bounce-in {
    animation: bounceIn 0.8s cubic-bezier(0.215, 0.610, 0.355, 1) both;
}

/* دخول متدرج بالارتداد لقائمة عناصر */
.list-item {
    opacity: 0;
    animation: bounceIn 0.6s ease-out both;
}

.list-item:nth-child(1) { animation-delay: 0s; }
.list-item:nth-child(2) { animation-delay: 0.1s; }
.list-item:nth-child(3) { animation-delay: 0.2s; }
.list-item:nth-child(4) { animation-delay: 0.3s; }
.list-item:nth-child(5) { animation-delay: 0.4s; }

تنويعات الظهور التدريجي

حركات ظهور تدريجي متعددة

/* ظهور تدريجي من الأسفل */
@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translateY(30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* ظهور تدريجي من الأعلى */
@keyframes fadeInDown {
    from {
        opacity: 0;
        transform: translateY(-30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

/* ظهور تدريجي مع تكبير */
@keyframes fadeInScale {
    from {
        opacity: 0;
        transform: scale(0.8);
    }
    to {
        opacity: 1;
        transform: scale(1);
    }
}

/* ظهور تدريجي مع ضبابية */
@keyframes fadeInBlur {
    from {
        opacity: 0;
        filter: blur(10px);
    }
    to {
        opacity: 1;
        filter: blur(0);
    }
}

/* تطبيق هذه كفئات مساعدة */
.fade-in-up { animation: fadeInUp 0.6s ease-out both; }
.fade-in-down { animation: fadeInDown 0.6s ease-out both; }
.fade-in-scale { animation: fadeInScale 0.5s ease-out both; }
.fade-in-blur { animation: fadeInBlur 0.7s ease-out both; }

عنصر نائب تحميل هيكلي

تأثير اللمعان للهياكل التحميلية

@keyframes shimmer {
    0% {
        background-position: -200% 0;
    }
    100% {
        background-position: 200% 0;
    }
}

.skeleton {
    background: linear-gradient(
        90deg,
        #e0e0e0 25%,
        #f0f0f0 50%,
        #e0e0e0 75%
    );
    background-size: 200% 100%;
    animation: shimmer 1.5s ease-in-out infinite;
    border-radius: 4px;
}

.skeleton-title {
    width: 60%;
    height: 24px;
    margin-bottom: 12px;
}

.skeleton-text {
    width: 100%;
    height: 16px;
    margin-bottom: 8px;
}

.skeleton-text:last-child {
    width: 80%;
}

إمكانية الوصول وتقليل الحركة

يمكن أن تسبب الحركات عدم الراحة أو الغثيان أو النوبات للأشخاص الذين يعانون من اضطرابات الدهليز أو حساسية الحركة أو الصرع الحساس للضوء. يوفر CSS الحديث استعلام الوسائط prefers-reduced-motion لاكتشاف متى يطلب المستخدم تقليل الحركة في إعدادات نظام التشغيل. يجب عليك دائما احترام هذا التفضيل.

احترام prefers-reduced-motion

/* النهج 1: إزالة جميع الحركات لمستخدمي تقليل الحركة */
@media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* النهج 2: توفير حركات بديلة خفيفة */
.hero-title {
    animation: bounceIn 0.8s ease-out both;
}

@media (prefers-reduced-motion: reduce) {
    .hero-title {
        animation: fadeIn 0.3s ease-out both;
        /* استبدال الدخول المرتد بظهور تدريجي بسيط */
    }
}

/* النهج 3: تحسين تدريجي -- إضافة الحركات فقط
   عند عدم تعيين تفضيل الحركة */
.card {
    opacity: 1; /* الافتراضي: بدون حركة */
}

@media (prefers-reduced-motion: no-preference) {
    .card {
        animation: fadeInUp 0.5s ease-out both;
    }
}
تحذير: لا تعتمد أبدا على الحركات وحدها لنقل معلومات مهمة. يجب أن تعزز الحركات تجربة المستخدم، وليس أن تكون الطريقة الوحيدة لفهم الواجهة. يجب أن يكون مؤشر التحميل الدوار مصحوبا بنص مثل "جاري التحميل..." لقارئات الشاشة، ويجب أن تقوم الإشعارات المتحركة أيضا بتحديث مناطق ARIA الحية.

التمرين 1: نظام إشعارات متحرك

ابنِ مكون إشعار بحركات الإطارات المفتاحية CSS. أنشئ بطاقة إشعار تنزلق من الجانب الأيمن من الشاشة باستخدام حركة slideInRight تستمر 0.4 ثانية مع دالة توقيت ease-out. استخدم animation-fill-mode: both بحيث يبقى العنصر في مكانه بعد التحريك. أضف شريط تقدم في أسفل الإشعار يتقلص من 100% عرض إلى 0% على مدى 5 ثوان باستخدام دالة توقيت خطية (هذا يعد تنازليا بصريا حتى يتم إخفاء الإشعار تلقائيا). بعد العد التنازلي لمدة 5 ثوان، أضف حركة slideOutRight بتأخير 5 ثوان لجعل الإشعار ينزلق مرة أخرى للخارج. أنشئ ثلاثة أنواع مختلفة من الإشعارات (نجاح، تحذير، خطأ) كل منها بلون نبض مميز على الحد الأيسر. إذا ظهرت إشعارات متعددة، وزعها عموديا بقيم animation-delay متزايدة. أخيرا، أضف استعلام prefers-reduced-motion يستبدل حركات الانزلاق بحركات ظهور تدريجي بسيطة.

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

أنشئ قسما رئيسيا لصفحة هبوط مع تسلسل حركات منسق. ابدأ بالظهور التدريجي لصورة الخلفية أو التدرج اللوني على مدى 1.5 ثانية. بعد استقرار الخلفية، حرك العنوان الرئيسي باستخدام حركة fadeInDown مع تأخير 0.5 ثانية. ثم أظهر العنوان الفرعي باستخدام fadeInUp مع تأخير 1 ثانية. بعد ذلك، حرك زر الدعوة للعمل بتأثير bounceIn مع تأخير 1.5 ثانية. بمجرد ظهور جميع العناصر، أضف حركة تعويم مستمرة خفيفة لعنصر زخرفي (مثل سهم أو أيقونة) تحركه لأعلى ولأسفل 10 بكسل باستخدام اتجاه alternate وتكرارات لا نهائية. أضف عنصرا زخرفيا ثانيا بنفس حركة التعويم لكن مع تأخير سالب قدره -1 ثانية بحيث يكون العنصران خارج الطور. استخدم animation-fill-mode: both على جميع حركات الدخول بحيث تكون العناصر مخفية أثناء فترة التأخير. تأكد من تعطيل التسلسل بأكمله أو تبسيطه عندما يكون prefers-reduced-motion: reduce نشطا.