أساسيات الواجهة الأمامية
انتقالات CSS وحركاتها: حركة تبدو صحيحة
دليل عملي كامل لحركة CSS — الانتقالات وأجزاؤها الأربعة، وما يمكن تحريكه وما لا يمكن، وtransform ولماذا هي الخاصية الأعلى أداءً، والتيسير وcubic-bezier، وحركات الإطارات المفتاحية وكل خصائص animation-*، والأداء (المُركِّب مقابل الخيط الرئيسي)، وprefers-reduced-motion، مع الأخطاء الشائعة وتمارين عملية مع حلولها.
الحركة هي ما يفصل صفحة تبدو ساكنة عن صفحة تبدو حيّة. تمنحك CSS أداتين: الانتقالات (transitions) للتحريك بين حالتين (تمرير، تبديل فتح/إغلاق)، والحركات (animations) بـ @keyframes لتسلسلات أغنى متعدّدة الخطوات أو متكرّرة. كلاهما يعمل في خطّ أنابيب رسم المتصفح بلا سطر JavaScript — وإن حرّكت الخصائص الصحيحة، عملت بنعومة حريرية خارج الخيط الرئيسي. هذه هي العُدّة الكاملة، مع قواعد الأداء والوصولية التي تفصل الحركة التي تُبهج عن الحركة التي تتقطّع. (هذا يقترن بمقال CSS الحديثة، الذي يغطّي طبقتي الحركة المدفوعة بالتمرير وانتقالات العرض الأحدث فوقها.)
قاعدة الأداء الوحيدة المهمّة: حرّك
transformو**opacity**، لاwidth/height/top/left. الأوّلان يتولّاهما المُركِّب (رخيص، خارج الخيط الرئيسي)؛ والبقية تفعّل التخطيط والرسم في كل إطار (مكلف، متقطّع).
الانتقالات: التحريك بين حالتين
الانتقال يراقب خاصية، وحين تتغيّر قيمتها، يحرّك إلى القيمة الجديدة عبر مدّة بدل القفز. تصرّح به على الحالة الأساس، فيُطبَّق في الاتجاهين (الدخول والخروج):
.btn {
background: royalblue;
transition: background 200ms ease;
}
.btn:hover {
background: crimson; /* التغيير ينتقل بسلاسة، عند التمرير وعند تركه */
}
للانتقال أربعة أجزاء — اختصار transition هو هذه بالترتيب:
transition: <property> <duration> <timing-function> <delay>;
transition: transform 300ms ease-in-out 0ms;
- property — ما يُراقَب (
allيراقب كل شيء، لكن كن محدّدًا للأداء) - duration — كم تطول (
200ms–300msتبدو صحيحة لمعظم الواجهات؛ الأطول يثقل) - timing-function — منحنى التيسير (أدناه)
- delay — الانتظار قبل البدء
تستطيع نقل عدّة خصائص دفعة بقائمة مفصولة بفواصل:
transition: transform 250ms ease, opacity 250ms ease, box-shadow 250ms ease;
قاعدة دقيقة لكنها مهمّة: صرّح بالانتقال على الحالة الساكنة، لا داخل :hover. ضعه على .btn، فيتحرّك الدخول والخروج من التمرير. ضعه في :hover فقط، فيتحرّك العنصر دخولًا لكنه يقفز عائدًا فورًا.
ما تستطيع (ولا تستطيع) تحريكه
ليست كل خاصية قابلة للتحريك. الخاصية قابلة للتحريك إن استطاع المتصفح حساب القيم الوسيطة — الألوان والأطوال والشفافية والتحويلات كلها تُستوفى بسلاسة. الخصائص المنفصلة مثل display لا تستطيع تاريخيًّا (لا يمكنك أن تكون نصف block)، وهذا السبب الكلاسيكي لحاجة التلاشي-ثم-الإخفاء إلى عناية:
/* ❌ لن يتحرّك — display ليس له منتصف */
.menu { display: none; transition: display 200ms; }
/* ✅ حرّك opacity + visibility بدلًا منه */
.menu {
opacity: 0;
visibility: hidden;
transition: opacity 200ms ease, visibility 200ms;
}
.menu.open { opacity: 1; visibility: visible; }
(CSS الحديثة تضيف transition-behavior: allow-discrete و@starting-style لتحريك display وحالات الدخول — انظر CSS الحديثة — لكن نمط opacity/visibility هو الحصان العامل المدعوم على نطاق واسع.)
transform: الخاصية التي ينبغي أن تحرّكها
transform يحرّك ويحجّم ويدوّر ويميل عنصرًا دون التأثير على التخطيط — لا يدفع الأشقّاء، والأهم أن المتصفح يستطيع تسليمه لمُركِّب الـ GPU. فضّله على تحريك top/left/width/height:
.card { transition: transform 200ms ease; }
.card:hover { transform: translateY(-4px) scale(1.02); }
/* تحويلات شائعة */
transform: translateX(20px); /* حرّك — استخدمه بدل left */
transform: scale(1.1); /* كبّر */
transform: rotate(15deg);
transform: translate(-50%, -50%); /* حيلة التوسيط الكلاسيكية مع absolute */
لماذا يهمّ: تحريك left يجبر المتصفح على إعادة حساب التخطيط وإعادة الرسم كل إطار؛ بينما تحريك transform: translateX() يُركِّب فقط طبقة مرسومة سلفًا — أرخص بكثير، وسلس حتى على الأجهزة الأضعف.
التيسير: جعل الحركة تبدو طبيعية
دالة التوقيت تشكّل كيف تتغيّر القيمة عبر المدّة. الحركة الخطّية تبدو آلية؛ الأشياء الحقيقية تتسارع وتتباطأ:
transition-timing-function: ease; /* الافتراضي — بداية ونهاية لطيفتان */
transition-timing-function: ease-out; /* بداية سريعة، هبوط ناعم — رائع لدخول الواجهة */
transition-timing-function: ease-in; /* بداية بطيئة — جيّد للأشياء المغادِرة */
transition-timing-function: linear; /* ثابت — مناسب للمؤشّرات الدوّارة/التقدّم */
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* مخصّص — تجاوز/ارتداد طفيف */
transition-timing-function: steps(4, end); /* قفزات منفصلة — حركة sprite */
ease-out هو الافتراضي الأأمن لمعظم حركة الواجهة — ينبغي أن تصل الأشياء بسرعة وتستقرّ بلطف. cubic-bezier() يتيح تصميم أي منحنى (شاملًا تجاوزًا مرحًا فوق 1)، وsteps() يُنتج قفزات إطارًا إطارًا لتأثيرات نمط sprite.
حركات الإطارات المفتاحية
حين لا تكفي حالتان — تحتاج محطّات متعدّدة أو تكرارًا أو حركة تعمل عند التحميل بلا مُطلِق — استخدم @keyframes وخصائص animation:
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
.dot {
animation: pulse 1.5s ease-in-out infinite;
}
اختصار animation يحزم حتى ثماني خصائص؛ وها هي مفصّلة:
.spinner {
animation-name: spin;
animation-duration: 1s;
animation-timing-function: linear;
animation-delay: 0s;
animation-iteration-count: infinite; /* أو عدد */
animation-direction: normal; /* normal | reverse | alternate */
animation-fill-mode: forwards; /* أبقِ حالة النهاية بعد الانتهاء */
animation-play-state: running; /* paused/running — بدّل بـ JS أو :hover */
}
@keyframes spin { to { transform: rotate(360deg); } }
خاصّيتان ينساهما الناس:
animation-fill-mode: forwardsيُبقي العنصر عند الإطار المفتاحي الأخير بدل القفز عائدًا إلى نمطه قبل الحركة — أساسي لـ "تحرّك دخولًا وابقَ".animation-direction: alternateيشغّل الإطارات أمامًا ثم خلفًا كل دورة — طريقة نظيفة لجعل نبضة أو طفو ينعكس بسلاسة بدل القفز.
استخدم انتقالًا حين تكون الحركة ردّ فعل على تغيّر حالة (تمرير، تبديل، إضافة صنف)؛ واستخدم حركة إطارات مفتاحية حين تكون الحركة ذاتية (مؤشّر تحميل، نبضة متكرّرة، دخول عند التحميل).
الأداء: المُركِّب مقابل الخيط الرئيسي
يرسم المتصفح على مراحل: التخطيط (layout) (حساب الهندسة) ← الرسم (paint) (ملء البكسلات) ← التركيب (composite) (تجميع الطبقات). كلّما كانت المرحلة التي تلمسها حركتك أرخص، عملت أنعم:
transformوopacity← تركيب فقط. الـ GPU يزيح طبقة موجودة؛ لا شيء يُعاد حسابه. هذا المسار السلس، حتى عند 60 إطارًا في الثانية على هاتف.width،height،top،left،margin،padding← تفعّل التخطيط كل إطار، ثم الرسم، ثم التركيب. على صفحة مزدحمة هذا يُسقط إطارات (تقطّع).color،background،box-shadow← تتخطّى التخطيط لكنها ما زالت ترسم كل إطار — متوسّطة.
فقاعدة "حرّك transform وopacity" ليست عقيدة أسلوبية — إنها حرفيًّا عن أي مراحل رسم تُطلَق كل إطار. وحين يجب أن تلمّح للمتصفح بترقية عنصر إلى طبقته الخاصة مسبقًا، استخدم will-change: transform بقلّة (الإفراط فيه يهدر الذاكرة):
.card { will-change: transform; } /* فقط على العناصر التي ستحرّكها */
احترم prefers-reduced-motion
بعض المستخدمين يصابون بالدوار أو الغثيان من الحركة (اضطرابات دهليزية)، ويتيح لهم نظام التشغيل طلب حركة أقلّ. احترمه — هذا متطلّب وصولية، لا رفاهية:
.card { transition: transform 200ms ease; }
.card:hover { transform: translateY(-4px); }
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
.card:hover { transform: none; }
/* أزل أو خفّف الحركة غير الأساسية */
}
نمط جيّد: صمّم الحركة عاديًّا، ثم أضف كتلة reduced-motion تزيل أو تقصّر الحركات غير الأساسية. أبقِ الحركة التي تنقل معنى (تغيّر حالة طفيف) لكن أسقط الحركة الزخرفية البحتة.
الأخطاء الشائعة
- تحريك
width/height/top/leftوالحصول على تقطّع — استخدمtransformللحركة والتحجيم. - التصريح بـ
transitionداخل:hoverبدل الحالة الأساس، فيقفز عائدًا فورًا عند مغادرة الفأرة. - استخدام
transition: allوتحريك خصائص مكلفة أو غير مقصودة بالخطأ — سمِّ الخصائص. - محاولة نقل
display: none↔blockورؤية قطع فوري — حرّكopacity/visibilityبدلًا منه. - نسيان
animation-fill-mode: forwards، فيقفز العنصر عائدًا إلى نمط بدايته عند انتهاء الحركة. - لصق
will-changeعلى كل شيء — يستهلك الذاكرة؛ استخدمه فقط قبيل التحريك. - مدد طويلة جدًّا (>400ms لتغذية الواجهة الراجعة) — ينبغي أن تبدو الحركة مستجيبة لا بطيئة.
- تجاهل
prefers-reduced-motion— إخفاق وصولية قد يُمرض المستخدمين فعلًا.
تمارين
جرّب كلًّا منها قبل فتح الحل.
تمرين 1 — رفع سلس عند التمرير
اجعل .card ترتفع 6px وتكتسب ظلًّا عند التمرير، متحرّكةً في الاتجاهين، باستخدام الخاصية الأعلى أداءً للحركة.
إظهار الحل
.card {
transition: transform 200ms ease, box-shadow 200ms ease;
}
.card:hover {
transform: translateY(-6px);
box-shadow: 0 12px 24px rgb(0 0 0 / 0.15);
}
الانتقال يعيش على الحالة الأساس فيتحرّك ترك التمرير أيضًا، وtranslateY (لا top) يُبقي الحركة على مسار المُركِّب الرخيص.
تمرين 2 — مؤشّر دوّار لا نهائي
اجعل .spinner يدور باستمرار بسرعة ثابتة.
إظهار الحل
@keyframes spin { to { transform: rotate(360deg); } }
.spinner {
animation: spin 0.8s linear infinite;
}
إطار to واحد يكفي (البداية هي حالة العنصر الحالية). linear يُبقي الدوران ثابتًا — التيسير سيجعل المؤشّر يتسارع ويتباطأ بشكل ظاهر.
تمرين 3 — تلاشٍ وصعود لقائمة عند الدخول
.menu تُبدَّل عبر صنف .open. لاشِها داخلًا وأزلِقها للأعلى 8px، واجعلها تختفي فعلًا (لا مجرّد تصير شفّافة) عند الإغلاق.
إظهار الحل
.menu {
opacity: 0;
visibility: hidden;
transform: translateY(8px);
transition: opacity 200ms ease, transform 200ms ease, visibility 200ms;
}
.menu.open {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
نقل visibility بجانب opacity يتيح للعنصر التلاشي والتوقّف عن تلقّي النقرات حالما يختفي — display لا يمكن نقله، فـ visibility يقوم بتلك المهمّة.
تمرين 4 — اجعلها وصولية
خذ رفع التمرير من التمرين 1 وعطّل الحركة للمستخدمين الذين يفضّلون حركة أقلّ.
إظهار الحل
@media (prefers-reduced-motion: reduce) {
.card { transition: none; }
.card:hover { transform: none; box-shadow: 0 4px 8px rgb(0 0 0 / 0.1); }
}
الحركة تُزال، لكن ظلًّا ساكنًا طفيفًا يمكن أن يبقى كي يظلّ التمرير مقروءًا كتفاعل — الحركة المخفّضة تعني حركة أقلّ، لا بالضرورة انعدام تغذية راجعة.
النموذج الذهني الذي تحتفظ به
تأتي حركة CSS في شكلين: الانتقالات تحرّك بين حالتين وتُصرَّح على الحالة الساكنة فتشتغل في الاتجاهين؛ وحركات الإطارات المفتاحية تقود حركة متعدّدة الخطوات أو متكرّرة أو عند التحميل عبر @keyframes وخصائص animation-* (لا تنسَ fill-mode: forwards لإمساك حالة النهاية). شكّل الإحساس بـ التيسير — ease-out للأشياء الواصلة، وlinear للمؤشّرات الدوّارة، وcubic-bezier() للمنحنيات المخصّصة. خيط الأداء بسيط وغير قابل للتفاوض: حرّك transform وopacity كي يبقى العمل على المُركِّب، وتجنّب تحريك خصائص التخطيط التي تُعيد الحساب كل إطار. وأخيرًا، لُفّ دائمًا الحركة غير الأساسية في حارس prefers-reduced-motion. أتقن ذلك وتتحرّك واجهاتك بسلاسة وهدف، وللجميع.