كن مطوّر واجهات أمامية محترفا
2 دقيقة قراءة

CSS الحديثة: الميزات التي غيّرت طريقة كتابتنا للأنماط

جولة في ميزات CSS الحديثة المتوفرة الآن في المتصفحات — استعلامات الحاوية و:has() والتداخل وطبقات التتالي وsubgrid والخصائص المخصّصة وcolor-mix() واللون الحديث والخصائص المنطقية وclamp() وaspect-ratio والحركات المدفوعة بالتمرير وانتقالات العرض و:is()/:where() — مع تمارين عملية.

توقّفت CSS في السنوات الأخيرة عن كونها اللغة التي تحاربها وصارت لغة يمكنك الاتكاء عليها. فئات كاملة من الحيل — clearfix، وJavaScript لـ "نسّق الأب"، وSass لمجرد تداخل المحددات، والأرقام السحرية للخطوط السائلة — استُبدلت بميزات أصيلة مدعومة الآن على نطاق واسع. هذه جولة في العُدّة الحديثة: ماذا تفعل كل ميزة، ومتى تلجأ إليها، والأسطر المفردة التي كانت تتطلب خطوة بناء.

CSS الحديثة تنقل العمل إلى المنصّة نفسها: التخطيط والتنسيق والتفاعلية وحتى انتقالات الصفحات صار لها أجوبة أصيلة وتعريفية — تبعيات أقل، وJavaScript أقل، وأداء أفضل.

الخصائص المخصّصة (متغيّرات CSS)

العمود الفقري لـ CSS الحديثة. متغيّرات حقيقية تعيش في التتالي، وتُورَّث، ويمكن قراءتها وتغييرها وقت التشغيل:

:root {
  --brand: oklch(62% 0.19 256);
  --space: 8px;
}
.card {
  padding: calc(var(--space) * 2);
  border: 1px solid var(--brand);
}
.card--danger {
  --brand: crimson;   /* تجاوز محلي؛ كل ما يستخدمه يتحدّث */
}

على عكس متغيّرات Sass (التي تُجمَّع وتختفي)، الخصائص المخصّصة حيّة — غيّر واحدة في media query أو :hover أو عبر JavaScript (element.style.setProperty) فتُعاد كل قيمة تابعة. وبها تُبنى السمات (theming) ورموز التصميم (design tokens) والوضع الداكن اليوم.

:has() — محدد الأب الذي طال انتظاره

طوال عقدين لم تستطع CSS التنسيق إلا نزولا. يتيح :has() للمحدد أن يعتمد على أحفاده أو أشقائه التالين — "محدد الأب" الذي طالب الناس به للأبد:

/* بطاقة تحتوي صورة تحصل على padding مختلف */
.card:has(img) { padding: 0; }

/* label مربع اختياره مفعّل */
label:has(input:checked) { font-weight: 600; }

/* نموذج فيه حقل غير صالح يُظهر لافتة الخطأ */
form:has(:invalid) .error-banner { display: block; }

:has() أكثر بكثير من محدد أب — إنه محدد علائقي، فيعبّر عن "نسّق A بناء على حالة B" بالكامل في CSS. وتختفي كميات هائلة من منطق "أضف class بـ JavaScript".

التداخل الأصيل (Nesting)

يمكنك الآن تداخل المحددات بلا Sass:

.card {
  padding: 16px;

  & .title { font-weight: 600; }

  &:hover { box-shadow: var(--shadow); }

  @media (min-width: 768px) {
    padding: 24px;
  }
}

الرمز & يشير إلى القاعدة الأب. ويزيل أحد أشيع أسباب جلب المشاريع لمعالج CSS مسبق أصلا. (أبقِ التداخل ضحلا — القواعد العميقة التداخل تصبح ثقيلة التخصيص وصعبة القراءة، تماما كما في Sass.)

طبقات التتالي (@layer)

حروب التخصيص — حيث ترفع محددا أو تضيف !important لمجرد الفوز — تُحلّ بنيويا بـ طبقات التتالي. تعلن ترتيبا صريحا، فتغلب الطبقات اللاحقة السابقةَ بغض النظر عن التخصيص:

@layer reset, framework, components, utilities;

@layer framework {
  .btn { padding: 8px 16px; }     /* محدد معقّد عالي التخصيص */
}
@layer utilities {
  .p-0 { padding: 0; }            /* يفوز رغم ذلك — طبقته لاحقة */
}

تتيح لك الطبقات ترويض CSS الطرف الثالث، وضمان وصول تجاوزاتك، والكفّ عن كتابة !important. الترتيب يُقرَّر مرة واحدة، بالنيّة، لا بمصادفة قوة المحدد.

:is() و:where() — التجميع بلا تكرار

/* قبل: مكرّر */
.content h1, .content h2, .content h3 { margin-top: 1.5em; }

/* بعد */
.content :is(h1, h2, h3) { margin-top: 1.5em; }

الفرق بينهما: :is() تأخذ تخصيص أكثر وسائطها تخصيصا، بينما :where() تخصيصها صفر دائما — ما يجعلها مثالية للافتراضيات منخفضة الأولوية التي يسهل تجاوزها للغاية.

اللون الحديث: oklch() وcolor-mix()

نضج لون CSS. تصف oklch() اللون في فضاء موحّد إدراكيا (إضاءة، تشبّع، تدرّج)، فتعديل "أفتح 10%" يبدو فعلا أفتح 10% — على عكس HSL. وcolor-mix() تمزج لونين داخل ورقة الأنماط مباشرة:

:root { --brand: oklch(62% 0.19 256); }

.btn:hover {
  /* 15% أبيض ممزوج في لون العلامة — درجة فاتحة بلا متغيّر إضافي */
  background: color-mix(in oklch, var(--brand), white 15%);
}
.muted {
  /* نسخة شفافة من أي لون، حتى لو كان متغيّرا */
  color: color-mix(in srgb, currentColor 60%, transparent);
}

معا تتيحان اشتقاق لوحة ألوان كاملة (تمريرات، درجات فاتحة، درجات داكنة، حدود) من لون أساسي واحد — ما كان يتطلب دوال ألوان Sass.

الخصائص المنطقية

الخصائص الفيزيائية left/right/top/bottom تفترض اتجاه كتابة. أما الخصائص المنطقية فتتبع تدفق النص، فتعمل نفس CSS في LTR وRTL بلا انعكاس:

.card {
  margin-inline: 16px;        /* يمين+يسار في LTR، ينقلب في RTL */
  padding-block: 12px;        /* أعلى+أسفل */
  border-inline-start: 2px solid; /* حافة "بداية" السطر */
  inset-inline-start: 0;      /* "يسار" منطقي للتموضع */
}

لأي موقع يُطلق في أكثر من اتجاه، تحذف الخصائص المنطقية فئة كاملة من الأخطاء — اكتب مرة، وانعكس تلقائيا.

التحجيم الذاتي: clamp() وmin() وmax() وaspect-ratio

تصميم سائل بلا كومة من media queries:

h1     { font-size: clamp(1.75rem, 1rem + 3vw, 3rem); }  /* خط سائل */
.wrap  { width: min(100% - 2rem, 1100px); margin-inline: auto; } /* سقف سائل */
.video { aspect-ratio: 16 / 9; }                          /* بلا حيلة padding */
.thumb { aspect-ratio: 1; object-fit: cover; }            /* مربعات مثالية */

aspect-ratio وحدها أحالت حيلة "padding-top: 56.25%" الشهيرة إلى التقاعد. وclamp() أحالت أكوام نقاط توقف حجم الخط إلى التقاعد. هذه قيم تتحجّم، فيتنفّس التخطيط بدل أن يقفز عند عروض ثابتة.

استعلامات الحاوية

الوثبة بعد media queries: يستطيع المكوّن الاستجابة لحجم حاويته هو بدل نافذة العرض، فتتكيّف نفس البطاقة سواء كانت في شريط جانبي ضيق أو عمود واسع.

.list { container-type: inline-size; }

@container (min-width: 400px) {
  .card { grid-template-columns: 120px 1fr; }
}

هذا ما يجعل المكوّنات قابلة لإعادة الاستخدام حقا — تحمل تجاوبها معها. (مشروح بعمق في مقال التصميم المتجاوب.)

Subgrid

يمكن للشبكة المتداخلة الآن أن تتحاذى مع مسارات أبيها عبر grid-template-rows: subgrid (أو الأعمدة)، فتتحاذى عناوين البطاقات وأجسامها وتذييلاتها عبر صف من بطاقات مستقلة — بلا ارتفاعات ثابتة ولا JavaScript. إنه القطعة الناقصة التي جعلت CSS Grid مكتملا لمكتبات المكوّنات.

الحركات المدفوعة بالتمرير

يمكن الآن ربط الحركات بموضع التمرير بلا JavaScript، وتعمل على المُركِّب (compositor) لسلاسة أعلى:

@keyframes fade-in {
  from { opacity: 0; transform: translateY(20px); }
  to   { opacity: 1; transform: none; }
}
.reveal {
  animation: fade-in linear both;
  animation-timeline: view();   /* التقدّم يقوده دخول العنصر نافذة العرض */
}

شريط تقدّم القراءة، أو الكشف-عند-التمرير، أو الـ parallax الذي كان يحتاج مستمع تمرير وrequestAnimationFrame، يصبح بضعة أسطر تعريفية — وأداء أفضل بكثير لأنه لا يلمس الخيط الرئيسي أبدا.

انتقالات العرض (View Transitions)

انتقالات متحركة بين حالتين — أو حتى بين صفحتين — مع قيام المتصفح بالمزج والتحوّل بدلا عنك:

@view-transition { navigation: auto; }   /* انتقالات عبر المستندات */

/* أعطِ عنصرا مشتركا اسما ليتحوّل بين الحالتين */
.hero-img { view-transition-name: hero; }
// لتغييرات الحالة داخل الصفحة
document.startViewTransition(() => updateTheDOM());

تأثير "الصورة المصغّرة تكبر بسلاسة إلى صفحة التفاصيل" الشبيه بالتطبيقات، أصيلا وببضعة أسطر، بدل مكتبة حركة ثقيلة.

بضع ميزات أخرى تستحق المعرفة

  • accent-color — لوّن مربعات الاختيار والراديو والـ ranges بخاصية واحدة.
  • :focus-visible — أظهر حلقات التركيز لمستخدمي لوحة المفاتيح دون وميضها عند نقرات الفأرة.
  • gap في flexbox وgrid — مسافات بلا انهيار حلّت محل حيل الـ margin.
  • inset — اختصار لـ top/right/bottom/left في سطر واحد.
  • text-wrap: balance / pretty — عناوين متوازنة وفقرات بلا أسطر يتيمة، مجانا.
  • :user-invalid — أنماط تحقّق لا تظهر إلا بعد تفاعل المستخدم، لا عند تحميل الصفحة.

أخطاء شائعة

  • اللجوء إلى JavaScript لـ "تنسيق الأب" بينما :has() يفعلها في CSS.
  • إضافة بناء Sass لمجرد تداخل ومتغيّرات صارت CSS تفعلها أصيلا.
  • محاربة التخصيص بـ !important بدل التنظيم بـ @layer.
  • كتابة حيل padding-top: 56.25% بينما aspect-ratio موجودة.
  • استخدام HSL لمقاييس الألوان والحصول على خطوات غير متساوية — oklch() موحّدة إدراكيا.
  • استخدام left/right في موقع ثنائي الاتجاه بدل المنطقية inline-start/inline-end.
  • تحريك تأثيرات التمرير في JavaScript بينما الحركات المدفوعة بالتمرير تعمل خارج الخيط الرئيسي.

تمارين

جرّب كل تمرين قبل فتح الحل. ملاحظة سريعة: بعضها (الحركات المدفوعة بالتمرير، انتقالات العرض) أحدث — اختبره في متصفح محدّث.

تمرين 1 — نسّق بطاقة بناء على محتواها بـ :has()

أعطِ أي .card تحتوي <img> padding صفرا، وأي .card بلا صورة padding بقيمة 16px — بلا classes إضافية، بلا JavaScript.

اعرض الحل
.card { padding: 16px; }      /* افتراضي للبطاقات بلا صورة */
.card:has(img) { padding: 0; } /* تجاوز للبطاقات التي تحتوي صورة */

:has(img) تطابق البطاقة بسبب حفيدها — المحدد العلائقي الذي كان يتطلب تبديل class بـ JavaScript.

تمرين 2 — درجة فاتحة ولون شفاف من متغيّر واحد

بمعلومية --brand، اصنع خلفية hover أفتح بـ 15% وحدّا هو العلامة بشفافية 30%، بلا إعلان أي متغيّرات لون جديدة.

اعرض الحل
.btn {
  background: var(--brand);
  border: 1px solid color-mix(in srgb, var(--brand) 30%, transparent);
}
.btn:hover {
  background: color-mix(in oklch, var(--brand), white 15%);
}

color-mix() تشتقّ الدرجة الفاتحة والحدّ الشفاف معا من اللون الأساسي الواحد — بلا دوال Sass ولا رموز إضافية.

تمرين 3 — حُلّ تعارض تخصيص بـ @layer

قاعدة إطار .btn { color: blue } تظل تغلب أداتك .text-red { color: red } بسبب ترتيب المصدر. اجعل الأداة تفوز دون !important ودون رفع التخصيص.

اعرض الحل
@layer framework, utilities;   /* utilities معلنة لاحقا ← أولوية أعلى */

@layer framework {
  .btn { color: blue; }
}
@layer utilities {
  .text-red { color: red; }   /* يفوز بغض النظر عن التخصيص */
}

لأن utilities طبقة لاحقة عن framework، تغلب كل قاعدة فيها كل قاعدة في الطبقة السابقة — ولم يعد التخصيص داخل الطبقات يقرّر الفائز عبر الطبقات.

تمرين 4 — hero سائل بلا media queries

عنوان hero يجب أن يتحجّم من 32px إلى 64px مع نافذة العرض، وصندوق الـ hero يجب أن يبقى دائما 16:9. بلا نقاط توقف، بلا حيل padding.

اعرض الحل
.hero {
  aspect-ratio: 16 / 9;
  display: grid;
  place-items: center;
}
.hero h1 {
  font-size: clamp(2rem, 1rem + 5vw, 4rem); /* 32px ← 64px، سائل */
}

aspect-ratio تشكّل الصندوق وclamp() تحجّم الخط — إعلانان يحلّان محل كومة media queries وحيلة النسبة القديمة.

تمرين 5 — كشف-عند-التمرير بلا JavaScript

اجعل كل .section يظهر بتلاشٍ-وصعود كلما دخل نافذة العرض بالتمرير، باستخدام حركة مدفوعة بالتمرير.

اعرض الحل
@keyframes rise {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: none; }
}
.section {
  animation: rise linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%; /* يُشغَّل أثناء دخول نافذة العرض */
}

/* احترم من يفضّلون حركة أقل */
@media (prefers-reduced-motion: reduce) {
  .section { animation: none; }
}

animation-timeline: view() يربط تقدّم الحركة بدخول العنصر نافذة العرض — بلا مستمع تمرير، يعمل خارج الخيط الرئيسي، ويحترم تقليل الحركة.

النموذج الذهني الذي تحتفظ به

CSS الحديثة تسحب العمل عائدا إلى المنصّة. ابنِ السمات على الخصائص المخصّصة، واشتقّ اللوحات بـ oklch() و**color-mix()، ونظّم التتالي بـ @layer بدل !important. نسّق علائقيا بـ :has()، وجمّع بـ :is()/:where()، وتداخل أصيلا. اجعل الأشياء تتحجّم بـ clamp()/min()/aspect-ratio، واجعل المكوّنات ذاتية التجاوب بـ استعلامات الحاوية وsubgrid**، واكتب CSS محايدة للاتجاه بـ الخصائص المنطقية. وللحركة والتنقّل، الجأ إلى الحركات المدفوعة بالتمرير وانتقالات العرض قبل مكتبة JavaScript. الخيط الجامع: إن كنت تعلّمت CSS قبل بضع سنوات، فعدد مفاجئ من حلولك الالتفافية القديمة صار له الآن جواب أصيل من سطر واحد.