تخطى إلى المحتوى
·22 دقيقة قراءة

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

استعلامات الحاوية، وطبقات CSS، و:has()، وcolor-mix()، والتداخل، وحركات التمرير، وتحديد موقع المرساة. ميزات CSS التي جعلتني أتوقف عن اللجوء إلى JavaScript.

مشاركة:X / TwitterLinkedIn

حذفت آخر ملف Sass قبل ستة أشهر. ليس لأنني كنت أتخذ موقفاً. لأنني حقاً لم أعد أحتاجه.

لأكثر من عقد، كانت CSS هي اللغة التي نعتذر عنها. كنا نحتاج لمعالجات أولية للتداخل والمتغيرات. كنا نحتاج JavaScript لتحديد الحجم على أساس الحاوية، والحركات المرتبطة بالتمرير، واختيار الأب، ونصف أنماط التخطيط التي يسلّمنا إياها المصممون. بنينا أنظمة تشغيل كاملة — مكتبات CSS-in-JS، وأطر عمل مساعدة، وسلاسل إضافات PostCSS — لتعويض ما لا تستطيع اللغة فعله أصلاً.

ذلك العصر انتهى. ليس "شارف على الانتهاء." ليس "في الطريق." انتهى.

الميزات التي هبطت في المتصفحات بين 2024 و 2026 لم تضف مجرد راحة. بل غيّرت النموذج الذهني. CSS لم تعد لغة أنماط تحاربها. إنها لغة أنماط تفكّر فعلاً في المكوّنات، وإدارة الخصوصية، والتصميم المتجاوب على مستوى العنصر، والحركات بدون وقت تشغيل.

إليك ما تغيّر، ولماذا يهم، وما يمكنك التوقف عن فعله بسببه.

استعلامات الحاوية: نهاية "ما عرض نافذة العرض؟"#

هذه هي الميزة التي غيّرت جذرياً طريقة تفكيري في التصميم المتجاوب. لعشرين عاماً، كان لدينا استعلامات الوسائط. استعلامات الوسائط تسأل: "ما عرض نافذة العرض؟" استعلامات الحاوية تسأل: "ما عرض الحاوية التي يعيش فيها هذا المكوّن؟"

يبدو هذا التمييز طفيفاً. ليس كذلك. إنه الفرق بين مكوّنات تعمل فقط في سياق تخطيط واحد ومكوّنات تعمل في كل مكان.

المشكلة التي وُجدت لعقدين#

فكّر في مكوّن بطاقة. في شريط جانبي، يجب أن يتكدّس عمودياً مع صورة صغيرة. في منطقة المحتوى الرئيسية، يجب أن يكون أفقياً مع صورة أكبر. في قسم بطل بعرض كامل، يجب أن يكون شيئاً آخر تماماً.

مع استعلامات الوسائط، كنت تكتب شيئاً كهذا:

css
/* The old way: coupling component styles to page layout */
.card {
  display: flex;
  flex-direction: column;
}
 
@media (min-width: 768px) {
  .sidebar .card {
    /* Still vertical in sidebar */
  }
 
  .main-content .card {
    flex-direction: row;
    /* Horizontal in main area */
  }
}
 
@media (min-width: 1200px) {
  .hero .card {
    /* Yet another layout */
  }
}

البطاقة تعرف عن .sidebar، و.main-content، و.hero. تعرف عن الصفحة. لم تعد مكوّناً — إنها جزء واعٍ بالصفحة. انقلها إلى صفحة مختلفة وينكسر كل شيء.

استعلامات الحاوية تحل هذا تماماً#

css
/* The container query way: component knows only about itself */
.card-wrapper {
  container-type: inline-size;
  container-name: card;
}
 
.card {
  display: grid;
  grid-template-columns: 1fr;
  gap: 1rem;
}
 
@container card (min-width: 400px) {
  .card {
    grid-template-columns: 200px 1fr;
  }
}
 
@container card (min-width: 700px) {
  .card {
    grid-template-columns: 300px 1fr;
    gap: 2rem;
    font-size: 1.125rem;
  }
}

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

inline-size مقابل size#

ستريد دائماً تقريباً container-type: inline-size. هذا يمكّن الاستعلامات على المحور السطري (العرض في أوضاع الكتابة الأفقية). استخدام container-type: size يمكّن استعلامات المحور السطري والكتلي معاً، لكنه يتطلب أن يكون للحاوية تحديد حجم صريح في كلا البعدين، مما يكسر التدفق الطبيعي للمستند في معظم الحالات.

css
/* This is what you want 99% of the time */
.wrapper {
  container-type: inline-size;
}
 
/* This requires explicit height — rarely what you want */
.wrapper-both {
  container-type: size;
  height: 500px; /* required, or it collapses */
}

الحاويات المسمّاة للسياقات المتداخلة#

عندما تتداخل الحاويات، تصبح التسمية ضرورية:

css
.page-layout {
  container-type: inline-size;
  container-name: layout;
}
 
.sidebar {
  container-type: inline-size;
  container-name: sidebar;
}
 
/* Target the sidebar specifically, not the nearest ancestor */
@container sidebar (max-width: 250px) {
  .nav-item {
    font-size: 0.875rem;
    padding: 0.25rem;
  }
}
 
/* Target the page layout */
@container layout (min-width: 1200px) {
  .page-header {
    font-size: 2.5rem;
  }
}

بدون أسماء، @container يستعلم أقرب سلف لديه احتواء. مع التداخل، غالباً لا تكون تلك الحاوية التي تريدها. سمِّها. دائماً.

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

هذه لا تحظى بالتقدير الكافي. وحدات استعلام الحاوية (cqi، cqb، cqw، cqh) تتيح لك تحديد حجم الأشياء نسبةً إلى الحاوية، وليس نافذة العرض:

css
@container (min-width: 400px) {
  .card-title {
    font-size: clamp(1rem, 4cqi, 2rem);
  }
}

4cqi هي 4% من الحجم السطري للحاوية. العنوان يتدرّج مع الحاوية، وليس النافذة. هذا ما كان يجب أن تكون عليه الطباعة المتدفقة منذ البداية.

طبقات CSS: حرب الخصوصية انتهت#

إذا غيّرت استعلامات الحاوية طريقة تفكيري في التصميم المتجاوب، فإن @layer غيّرت طريقة تفكيري في هندسة CSS. لأول مرة، لدينا طريقة عاقلة وتصريحية لإدارة الخصوصية عبر مشروع كامل.

المشكلة#

خصوصية CSS هي نظام نقاط لا يهتم بنواياك. صنف مساعد .text-red يخسر أمام .card .title لأن الأخير لديه خصوصية أعلى. الحل كان دائماً نفسه: اجعل محدّداتك أكثر تحديداً، أضف !important، أو أعد هيكلة كل شيء.

بنينا منهجيات كاملة (BEM، SMACSS، ITCSS) وسلاسل أدوات فقط لتجنب تعارضات الخصوصية. كل ذلك كان حلاً بديلاً لميزة لغوية مفقودة.

ترتيب الطبقات#

@layer يتيح لك الإعلان عن الترتيب الذي تُراعى فيه مجموعات الأنماط، بغض النظر عن الخصوصية ضمن تلك المجموعات:

css
/* Declare layer order — this single line controls everything */
@layer reset, base, components, utilities, overrides;
 
@layer reset {
  *,
  *::before,
  *::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
  }
}
 
@layer base {
  body {
    font-family: system-ui, sans-serif;
    line-height: 1.6;
    color: oklch(20% 0 0);
  }
 
  h1, h2, h3 {
    line-height: 1.2;
    text-wrap: balance;
  }
}
 
@layer components {
  .card {
    background: white;
    border-radius: 0.5rem;
    padding: 1.5rem;
    box-shadow: 0 1px 3px oklch(0% 0 0 / 0.12);
  }
 
  .card .title {
    font-size: 1.25rem;
    font-weight: 600;
    color: oklch(25% 0.05 260);
  }
}
 
@layer utilities {
  .text-red {
    color: oklch(55% 0.25 25);
  }
 
  .hidden {
    display: none;
  }
 
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
  }
}

رغم أن .card .title لديها خصوصية أعلى من .text-red، يفوز المساعد لأن طبقة utilities مُعلنة بعد components. لا !important. لا حيل خصوصية. ترتيب الطبقات هو الكلمة الأخيرة.

كيف يستخدم Tailwind CSS v4 الطبقات#

Tailwind v4 يعتمد بشكل كبير على @layer. عندما تكتب @import "tailwindcss"، تحصل على:

css
@layer theme, base, components, utilities;

كل مساعد Tailwind يعيش في طبقة utilities. أنماط المكوّنات المخصصة تذهب في components. لهذا السبب يمكن لصنف text-red-500 تجاوز لون مكوّن بدون !important — إنه في طبقة لاحقة.

إذا كنت تبني نظام تصميمك الخاص بدون Tailwind، استعر هذه الهندسة. إنها الصحيحة:

css
@layer reset, tokens, base, layouts, components, utilities, overrides;

سبع طبقات كافية. لم أحتج أكثر أبداً.

الأنماط غير المُطبّقة تتفوق على كل شيء#

تحذير واحد: الأنماط التي ليست في أي طبقة لها الأولوية الأعلى. هذا مفيد فعلاً — يعني أن تجاوزاتك المحددة للصفحة تفوز تلقائياً:

css
@layer components {
  .modal {
    background: white;
  }
}
 
/* Not in any layer — wins over everything in layers */
.special-page .modal {
  background: oklch(95% 0.02 260);
}

لكنه يعني أيضاً أن CSS الطرف الثالث غير الواعية بالطبقات يمكن أن تتجاوز نظامك بالكامل. غلّف أنماط الطرف الثالث في طبقة للتحكم فيها:

css
@layer third-party {
  @import url("some-library.css");
}

محدّد :has(): محدّد الأب الذي أردناه دائماً#

لعقود حرفياً، طلب المطورون محدّد أب. "أريد تنسيق أب بناءً على أبنائه." كان الجواب دائماً "CSS لا تستطيع فعل ذلك" يليه حل بديل بـ JavaScript. :has() يغيّر هذا تماماً، واتضح أنه أقوى حتى مما طلبناه.

اختيار الأب الأساسي#

css
/* Style a form group when its input has focus */
.form-group:has(input:focus) {
  border-color: oklch(55% 0.2 260);
  box-shadow: 0 0 0 3px oklch(55% 0.2 260 / 0.15);
}
 
/* Style a card differently when it contains an image */
.card:has(img) {
  padding-top: 0;
}
 
.card:has(img) .card-content {
  padding: 1.5rem;
}
 
/* A card without an image gets different treatment */
.card:not(:has(img)) {
  border-left: 4px solid oklch(55% 0.2 260);
}

حالات التحقق من النماذج بدون JavaScript#

هنا يصبح :has() مثيراً حقاً. بالدمج مع أصناف التحقق الزائفة لـ HTML، يمكنك بناء واجهات نماذج تستجيب لحالة الصحة بدون JavaScript على الإطلاق:

css
/* The field wrapper reacts to its input's validity */
.field:has(input:invalid:not(:placeholder-shown)) {
  --field-color: oklch(55% 0.25 25);
}
 
.field:has(input:valid:not(:placeholder-shown)) {
  --field-color: oklch(55% 0.2 145);
}
 
.field {
  --field-color: oklch(70% 0 0);
  border: 2px solid var(--field-color);
  border-radius: 0.5rem;
  padding: 0.75rem;
  transition: border-color 0.2s;
}
 
.field label {
  color: var(--field-color);
  font-size: 0.875rem;
  font-weight: 500;
}
 
.field .error-message {
  display: none;
  color: oklch(55% 0.25 25);
  font-size: 0.8rem;
  margin-top: 0.25rem;
}
 
.field:has(input:invalid:not(:placeholder-shown)) .error-message {
  display: block;
}

جزء :not(:placeholder-shown) حاسم — يمنع أنماط التحقق من الظهور على الحقول الفارغة التي لم تُلمس بعد.

التخطيط بناءً على عدد الأبناء#

هذا النمط مفيد بشكل سخيف وكان مستحيلاً فعلاً قبل :has():

css
/* Adjust grid columns based on how many items exist */
.grid-auto:has(> :nth-child(4)) {
  grid-template-columns: repeat(2, 1fr);
}
 
.grid-auto:has(> :nth-child(7)) {
  grid-template-columns: repeat(3, 1fr);
}
 
.grid-auto:has(> :nth-child(13)) {
  grid-template-columns: repeat(4, 1fr);
}

الشبكة تغيّر عدد أعمدتها بناءً على عدد أبنائها. لا JavaScript. لا ResizeObserver. لا تبديل أصناف.

اكتشاف الشريط الجانبي#

أحد أنماطي المفضلة — تغيير التخطيط بناءً على ما إذا كان الشريط الجانبي موجوداً:

css
.page-layout {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2rem;
}
 
/* If the layout contains a sidebar, switch to two columns */
.page-layout:has(.sidebar) {
  grid-template-columns: 1fr 300px;
}
 
/* Adjust main content width when sidebar is present */
.page-layout:has(.sidebar) .main-content {
  max-width: 65ch;
}

أضف مكوّن شريط جانبي إلى DOM، يتكيّف التخطيط. أزله، يتكيّف مرة أخرى. CSS هي مصدر الحقيقة، وليس متغير حالة.

دمج :has() مع محدّدات أخرى#

:has() يتركّب بشكل جميل مع كل شيء آخر:

css
/* Style an article only when it has a specific class of figure */
article:has(figure.full-bleed) {
  overflow: visible;
}
 
/* A navigation that changes when it contains a search input */
nav:has(input[type="search"]:focus) {
  background: oklch(98% 0 0);
  box-shadow: 0 2px 8px oklch(0% 0 0 / 0.08);
}
 
/* Enable dark-mode at the component level */
.theme-switch:has(input:checked) ~ main {
  color-scheme: dark;
  background: oklch(15% 0 0);
  color: oklch(90% 0 0);
}

دعم المتصفحات ممتاز الآن. كل متصفح حديث دعم :has() منذ أوائل 2024. لا يوجد سبب لعدم استخدامه.

تداخل CSS: أصلي، مستقر أخيراً#

لن أبالغ في بيع هذه الميزة. تداخل CSS لطيف. ليس ثورياً بالطريقة التي تكون بها استعلامات الحاوية أو :has(). لكنه يزيل أحد آخر الأسباب لاستخدام Sass، وهذا مهم.

الصياغة#

css
.card {
  background: white;
  border-radius: 0.5rem;
  padding: 1.5rem;
 
  .title {
    font-size: 1.25rem;
    font-weight: 600;
    margin-bottom: 0.5rem;
  }
 
  .description {
    color: oklch(40% 0 0);
    line-height: 1.7;
  }
 
  &:hover {
    box-shadow: 0 4px 12px oklch(0% 0 0 / 0.1);
  }
 
  &.featured {
    border: 2px solid oklch(60% 0.2 260);
  }
}

هذا CSS صالح. لا خطوة بناء. لا معالج أولي. لا إضافة PostCSS. المتصفح يتعامل معه أصلاً.

الفروقات عن Sass#

هناك بعض فروقات الصياغة التي يجدر معرفتها:

css
/* CSS Nesting — works now in all browsers */
.parent {
  /* Direct class/element nesting works without & */
  .child {
    color: red;
  }
 
  /* & is required for pseudo-classes and compound selectors */
  &:hover {
    background: blue;
  }
 
  &.active {
    font-weight: bold;
  }
 
  /* Nested media queries — this is great */
  @media (width >= 768px) {
    flex-direction: row;
  }
}

في التنفيذات المبكرة، كنت تحتاج & قبل محدّدات العناصر (مثل & p بدلاً من فقط p). هذا القيد تمت إزالته. منذ 2025، جميع المتصفحات الرئيسية تدعم تداخل العناصر المجرد: .parent { p { ... } } يعمل بشكل جيد.

استعلامات الوسائط المتداخلة#

هذه هي الميزة القاتلة لتداخل CSS، في رأيي. ليس تداخل المحدّدات — القدرة على وضع استعلامات الوسائط داخل كتلة قاعدة:

css
.hero {
  padding: 2rem;
  font-size: 1rem;
 
  @media (width >= 768px) {
    padding: 4rem;
    font-size: 1.25rem;
  }
 
  @media (width >= 1200px) {
    padding: 6rem;
    font-size: 1.5rem;
  }
}

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

لا تبالغ في التداخل#

تحذير واحد: لمجرد أنك تستطيع التداخل ستة مستويات لا يعني أنه يجب عليك ذلك. نفس النصيحة من Sass تنطبق هنا. إذا كان تداخلك ينشئ محدّدات مثل .page .section .card .content .text .highlight، فقد أنشأت وحشاً من الخصوصية وكابوس صيانة. مستويان أو ثلاثة هي النقطة المثالية.

css
/* Good — two levels */
.nav {
  .link {
    color: inherit;
 
    &:hover {
      color: oklch(55% 0.2 260);
    }
  }
}
 
/* Bad — specificity nightmare */
.page {
  .layout {
    .sidebar {
      .nav {
        .list {
          .item {
            .link {
              color: red; /* good luck overriding this */
            }
          }
        }
      }
    }
  }
}

دوال الألوان: oklch() و color-mix() تغيّران كل شيء#

hsl() قدّمت عرضاً جيداً. كانت أكثر بديهية من rgb(). لكن لديها عيب أساسي: ليست موحّدة إدراكياً. hsl(60, 100%, 50%) (أصفر) تبدو أفتح بشكل كبير للعين البشرية من hsl(240, 100%, 50%) (أزرق)، رغم أن لهما نفس قيمة الإضاءة.

لماذا تفوز oklch()#

oklch() موحّدة إدراكياً. قيم الإضاءة المتساوية تبدو متساوية الإضاءة. هذا مهم بشكل كبير عند توليد لوحات الألوان، وإنشاء السمات، وضمان تباين يمكن الوصول إليه:

css
:root {
  /* oklch(lightness chroma hue) */
  --color-primary: oklch(55% 0.2 260);     /* blue */
  --color-secondary: oklch(55% 0.2 330);   /* purple */
  --color-success: oklch(55% 0.2 145);     /* green */
  --color-danger: oklch(55% 0.25 25);      /* red */
  --color-warning: oklch(75% 0.18 85);     /* yellow — higher L for equal perception */
 
  /* These all look equally "medium" to the human eye */
  /* With HSL, you'd need different lightness values for each hue */
}

القيم الثلاث بديهية بمجرد فهمها:

  • الإضاءة (0% إلى 100%): كم هو فاتح أو داكن. 0% أسود، 100% أبيض.
  • الكروما (0 إلى ~0.37): كم هو حيوي. 0 رمادي، أعلى يعني أكثر تشبّعاً.
  • درجة اللون (0 إلى 360): زاوية اللون. 0/360 أحمر وردي، 145 أخضر، 260 أزرق.

color-mix() للألوان المشتقة#

color-mix() يتيح لك إنشاء ألوان من ألوان أخرى في وقت التشغيل. لا دالة darken() من Sass. لا JavaScript. فقط CSS:

css
:root {
  --brand: oklch(55% 0.2 260);
 
  /* Lighten by mixing with white */
  --brand-light: color-mix(in oklch, var(--brand) 30%, white);
 
  /* Darken by mixing with black */
  --brand-dark: color-mix(in oklch, var(--brand) 70%, black);
 
  /* Create a subtle background */
  --brand-bg: color-mix(in oklch, var(--brand) 8%, white);
 
  /* Semi-transparent version */
  --brand-overlay: color-mix(in oklch, var(--brand) 50%, transparent);
}
 
.button {
  background: var(--brand);
  color: white;
 
  &:hover {
    background: var(--brand-dark);
  }
 
  &:active {
    background: color-mix(in oklch, var(--brand) 60%, black);
  }
}
 
.button.secondary {
  background: var(--brand-bg);
  color: var(--brand-dark);
 
  &:hover {
    background: color-mix(in oklch, var(--brand) 15%, white);
  }
}

جزء in oklch مهم. الخلط في srgb يعطي ألواناً وسيطة عكرة. الخلط في oklch يعطي نتائج متساوية إدراكياً. اخلط دائماً في oklch.

بناء لوحة ألوان كاملة مع oklch()#

إليك كيف أولّد لوحة درجات كاملة من درجة لون واحدة:

css
:root {
  --hue: 260;
  --chroma: 0.2;
 
  --color-50:  oklch(97% calc(var(--chroma) * 0.1) var(--hue));
  --color-100: oklch(93% calc(var(--chroma) * 0.2) var(--hue));
  --color-200: oklch(85% calc(var(--chroma) * 0.4) var(--hue));
  --color-300: oklch(75% calc(var(--chroma) * 0.6) var(--hue));
  --color-400: oklch(65% calc(var(--chroma) * 0.8) var(--hue));
  --color-500: oklch(55% var(--chroma) var(--hue));
  --color-600: oklch(48% var(--chroma) var(--hue));
  --color-700: oklch(40% calc(var(--chroma) * 0.9) var(--hue));
  --color-800: oklch(32% calc(var(--chroma) * 0.8) var(--hue));
  --color-900: oklch(25% calc(var(--chroma) * 0.7) var(--hue));
  --color-950: oklch(18% calc(var(--chroma) * 0.5) var(--hue));
}

غيّر --hue إلى 145 وتحصل على لوحة خضراء. غيّرها إلى 25 وتحصل على أحمر. خطوات الإضاءة متساوية إدراكياً. الكروما يتناقص عند الأطراف حتى لا تكون أفتح وأغمق الدرجات مفرطة التشبّع. هذا من الأشياء التي كانت تتطلب أداة تصميم أو دالة Sass. الآن هي ثمانية سطور CSS.

حركات التمرير: لا JavaScript مطلوب#

هذه الميزة هي التي جعلتني أحذف أكثر JavaScript. الحركات المرتبطة بالتمرير — أشرطة التقدم، تأثيرات المنظور، حركات الظهور، الترويسات اللاصقة مع الانتقالات — كانت تتطلب IntersectionObserver، أو مستمعات حدث scroll، أو مكتبة مثل GSAP. الآن هي CSS.

مؤشر تقدم التمرير#

شريط "تقدم القراءة" الكلاسيكي في أعلى صفحة المقالة:

css
.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  background: oklch(55% 0.2 260);
  transform-origin: left;
  z-index: 1000;
 
  animation: grow-progress linear both;
  animation-timeline: scroll();
}
 
@keyframes grow-progress {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

هذا كل شيء. مؤشر تقدم القراءة بالكامل. لا JavaScript. لا مستمع حدث تمرير. لا requestAnimationFrame. لا حساب "نسبة التمرير". ربط animation-timeline: scroll() يفعل كل شيء.

الظهور عند التمرير#

عناصر تتلاشى إلى الظهور عند دخولها نافذة العرض:

css
.reveal {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}
 
@keyframes reveal {
  from {
    opacity: 0;
    transform: translateY(30px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

animation-timeline: view() يربط الحركة بمرئية العنصر في نافذة العرض. animation-range: entry 0% entry 100% يعني أن الحركة تعمل من لحظة بدء دخول العنصر لنافذة العرض حتى يكون مرئياً تماماً.

المنظور بدون مكتبة#

css
.parallax-bg {
  animation: parallax linear both;
  animation-timeline: scroll();
}
 
@keyframes parallax {
  from {
    transform: translateY(-20%);
  }
  to {
    transform: translateY(20%);
  }
}
 
.parallax-section {
  overflow: hidden;
  position: relative;
}

صورة الخلفية تتحرك بمعدل مختلف عن التمرير، مما ينشئ تأثير منظور. سلس، عالي الأداء (المتصفح يمكنه التركيب على GPU)، وبدون JavaScript.

خطوط زمنية للتمرير المسمّاة#

لمزيد من التحكم، يمكنك تسمية خطوط زمنية للتمرير والإشارة إليها من عناصر أخرى:

css
.scroller {
  overflow-y: auto;
  scroll-timeline-name: --my-scroller;
  scroll-timeline-axis: block;
}
 
/* An element anywhere in the DOM can reference this timeline */
.indicator {
  animation: progress linear both;
  animation-timeline: --my-scroller;
}
 
@keyframes progress {
  from { width: 0%; }
  to { width: 100%; }
}

هذا يعمل حتى عندما لا يكون المؤشر والمُمرّر أب/ابن. أي عنصر يمكنه الربط بأي خط زمني تمرير مسمّى. هذا قوي لواجهات لوحات التحكم حيث لوحة قابلة للتمرير تقود مؤشراً في ترويسة ثابتة.

تحديد موقع المرساة: التلميحات والنوافذ المنبثقة بالشكل الصحيح#

قبل تحديد موقع المرساة، كان ربط تلميح بعنصره المُشغِّل يتطلب JavaScript. كنت تحسب المواقع مع getBoundingClientRect()، وتتعامل مع إزاحات التمرير، وتدير تصادمات نافذة العرض، وتعيد الحساب عند تغيير الحجم. مكتبات مثل Popper.js (الآن Floating UI) وُجدت تحديداً لأن هذا كان صعباً جداً.

تحديد موقع المرساة في CSS يجعل هذا تصريحياً:

css
.trigger {
  anchor-name: --my-trigger;
}
 
.tooltip {
  position: fixed;
  position-anchor: --my-trigger;
 
  /* Position the tooltip's top-center at the trigger's bottom-center */
  top: anchor(bottom);
  left: anchor(center);
  translate: -50% 0;
 
  /* Fallback positioning if it overflows the viewport */
  position-try-fallbacks: flip-block, flip-inline;
}

معالجة تصادم نافذة العرض تلقائياً#

خاصية position-try-fallbacks هي الجزء الذي كان سيتطلب 200 سطر من JavaScript. تخبر المتصفح: "إذا تجاوز التلميح نافذة العرض من الأسفل، اقلبه للأعلى. إذا تجاوز من اليمين، اقلبه لليسار."

css
.dropdown-menu {
  position: fixed;
  position-anchor: --menu-button;
 
  /* Default: below the button, aligned to the left edge */
  top: anchor(bottom);
  left: anchor(left);
 
  /* If it doesn't fit below, try above. If it doesn't fit left-aligned, try right-aligned */
  position-try-fallbacks: flip-block, flip-inline;
 
  /* Add a gap between the anchor and the dropdown */
  margin-top: 4px;
}

بدائل المواقع المسمّاة#

لمزيد من التحكم في مواقع البديل، يمكنك تعريف خيارات محاولة مخصصة:

css
@position-try --above-right {
  bottom: anchor(top);
  right: anchor(right);
  top: auto;
  left: auto;
  margin-top: 0;
  margin-bottom: 4px;
}
 
@position-try --left-center {
  right: anchor(left);
  top: anchor(center);
  left: auto;
  bottom: auto;
  translate: 0 -50%;
  margin-top: 0;
  margin-right: 4px;
}
 
.tooltip {
  position: fixed;
  position-anchor: --trigger;
  top: anchor(bottom);
  left: anchor(center);
  translate: -50% 0;
  margin-top: 4px;
 
  position-try-fallbacks: --above-right, --left-center;
}

المتصفح يجرّب كل بديل بالترتيب حتى يجد واحداً يبقي العنصر ضمن نافذة العرض. هذا النوع من التفكير المكاني كان مؤلماً حقاً في JavaScript.

التثبيت مع Popover API#

تحديد موقع المرساة يتزاوج بشكل مثالي مع Popover API الجديد:

html
<button popovertarget="my-popover" style="anchor-name: --btn">Settings</button>
 
<div popover id="my-popover" style="
  position-anchor: --btn;
  top: anchor(bottom);
  left: anchor(center);
  translate: -50% 0;
  margin-top: 8px;
">
  Popover content here
</div>

لا JavaScript للإظهار/الإخفاء (Popover API يتعامل مع ذلك). لا JavaScript لتحديد الموقع (تحديد موقع المرساة يتعامل مع ذلك). لا JavaScript لسلوك الإغلاق بالنقر خارجاً (Popover API يتعامل مع ذلك أيضاً). نمط التلميح/النافذة المنبثقة/القائمة المنسدلة بالكامل — النمط الذي شغّل حزم npm كاملة — هو الآن HTML و CSS.

CSS Grid Subgrid: المحاذاة المتداخلة التي تعمل فعلاً#

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

Subgrid يصلح هذا.

مشكلة محاذاة البطاقات#

css
/* Parent grid */
.card-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 1.5rem;
}
 
/* Each card becomes a subgrid, aligning rows to the parent */
.card {
  display: grid;
  grid-row: span 3; /* Card spans 3 rows in the parent */
  grid-template-rows: subgrid;
  gap: 0; /* Card controls its own internal gap */
}
 
.card-title {
  font-weight: 600;
  padding: 1rem 1rem 0.5rem;
}
 
.card-body {
  padding: 0 1rem;
  color: oklch(40% 0 0);
}
 
.card-footer {
  padding: 0.5rem 1rem 1rem;
  margin-top: auto;
  border-top: 1px solid oklch(90% 0 0);
}

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

قبل Subgrid: الحيل#

بدون subgrid، تحقيق هذه المحاذاة كان يتطلب إما:

  1. ارتفاعات ثابتة (هشة، تنكسر مع المحتوى الديناميكي)
  2. قياس JavaScript (بطيء، وميض عدم المحاذاة)
  3. الاستسلام وقبول عدم المحاذاة (شائع، قبيح)
css
/* The old hack — fragile and breaks with dynamic content */
.card-title {
  min-height: 3rem; /* pray that no title exceeds this */
}
 
.card-body {
  min-height: 8rem; /* even more prayer */
}

Subgrid يجعل الحيلة غير ضرورية. شبكة الأب توزّع ارتفاعات الصفوف بناءً على أطول محتوى في كل صف عبر جميع البطاقات.

Subgrid للأعمدة لتخطيطات النماذج#

Subgrid يعمل على الأعمدة أيضاً، وهو مثالي لتخطيطات النماذج:

css
.form {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.75rem 1rem;
}
 
.form-row {
  display: grid;
  grid-column: 1 / -1;
  grid-template-columns: subgrid;
  align-items: center;
}
 
.form-row label {
  grid-column: 1;
  text-align: right;
  font-weight: 500;
}
 
.form-row input {
  grid-column: 2;
  padding: 0.5rem;
  border: 1px solid oklch(80% 0 0);
  border-radius: 0.375rem;
}

جميع العناوين تتحاذى. جميع الحقول تتحاذى. عمود العناوين يُحدَّد تلقائياً لأعرض عنوان. لا عرض مُثبّت.

View Transitions API: تنقّل شبيه بـ SPA بدون إطار عمل#

هذه أكثر ميزة طموحة في هذه القائمة. View Transitions API يتيح لك تحريك الانتقال بين التنقلات — بما في ذلك التنقلات عبر المستندات (نقرات الروابط العادية في تطبيق متعدد الصفحات). موقعك HTML الثابت يمكنه الآن أن يحتوي على انتقالات صفحات سلسة ومتحركة.

الانتقالات عبر المستندات#

لتمكين انتقالات العرض عبر المستندات، تضيف قاعدة CSS واحدة لكل من الصفحة القديمة والجديدة:

css
@view-transition {
  navigation: auto;
}

هذا كل شيء. المتصفح سيقوم الآن بتلاشي متقاطع بين الصفحات عند التنقل. الانتقال الافتراضي هو تلاشي شفافية سلس. لا JavaScript. لا إطار عمل. فقط سطران من CSS في كل صفحة.

تخصيص الانتقال#

يمكنك تخصيص أي انتقالات تحدث بتسمية عناصر محددة:

css
/* On both pages */
.page-header {
  view-transition-name: header;
}
 
.main-content {
  view-transition-name: content;
}
 
.hero-image {
  view-transition-name: hero;
}
 
/* Transition animations */
::view-transition-old(content) {
  animation: slide-out 0.3s ease-in both;
}
 
::view-transition-new(content) {
  animation: slide-in 0.3s ease-out both;
}
 
::view-transition-old(hero) {
  animation: fade-out 0.2s ease-in both;
}
 
::view-transition-new(hero) {
  animation: none; /* The new hero just appears */
}
 
@keyframes slide-out {
  to {
    transform: translateX(-100%);
    opacity: 0;
  }
}
 
@keyframes slide-in {
  from {
    transform: translateX(100%);
    opacity: 0;
  }
}
 
@keyframes fade-out {
  to {
    opacity: 0;
  }
}

عند التنقل بين صفحتين تحتويان على .hero-image مع view-transition-name: hero، يحرّك المتصفح تلقائياً الصورة من موقعها في الصفحة القديمة إلى موقعها في الصفحة الجديدة. إنه نمط "انتقال العنصر المشترك" من تطوير الهاتف المحمول، الآن في المتصفح.

انتقالات العرض في SPA#

لتطبيقات الصفحة الواحدة (React، Vue، Svelte، إلخ)، واجهة JavaScript البرمجية مباشرة:

css
/* CSS side — define your transition animations */
::view-transition-old(root) {
  animation: fade-out 0.2s ease-in;
}
 
::view-transition-new(root) {
  animation: fade-in 0.2s ease-out;
}
javascript
// JS side — wrap your DOM update in startViewTransition
document.startViewTransition(() => {
  // Update the DOM here — React render, innerHTML swap, etc.
  updateContent(newPageData);
});

المتصفح يلتقط حالة قديمة، يشغّل تحديثك، يلتقط الحالة الجديدة، ويحرّك بينهما. العناصر المسمّاة تحصل على انتقالات فردية؛ كل شيء آخر يحصل على تلاشي متقاطع افتراضي.

احترام تفضيلات المستخدم#

احترم دائماً prefers-reduced-motion:

css
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

ما يمكنك التوقف عن استخدامه#

هذه الميزات مجتمعة تزيل الكثير من الأدوات والمكتبات والأنماط التي اعتمدنا عليها لسنوات. إليك ما أزلته أو توقفت عن اللجوء إليه:

Sass/SCSS للتداخل والمتغيرات#

CSS لديها تداخل أصلي. CSS لديها خصائص مخصصة (ولديها منذ سنوات). السببان الرئيسيان الذين جعلا الناس يلجأون إلى Sass موجودان الآن في اللغة. إذا كنت لا تزال تستخدم Sass فقط لـ $variables والتداخل، يمكنك التوقف.

css
/* Before: Sass */
$primary: #3b82f6;
 
.card {
  background: white;
  border: 1px solid lighten($primary, 40%);
 
  &:hover {
    border-color: $primary;
  }
 
  .title {
    color: darken($primary, 15%);
  }
}
 
/* After: Native CSS */
.card {
  --primary: oklch(55% 0.2 260);
 
  background: white;
  border: 1px solid color-mix(in oklch, var(--primary) 20%, white);
 
  &:hover {
    border-color: var(--primary);
  }
 
  .title {
    color: color-mix(in oklch, var(--primary) 70%, black);
  }
}

نسخة CSS أقوى — oklch تعطي معالجة ألوان موحّدة إدراكياً، color-mix تعمل في وقت التشغيل (Sass lighten في وقت التجميع فقط)، والخصائص المخصصة يمكن تغييرها ديناميكياً من JavaScript أو استعلامات الوسائط.

JavaScript لحركات التمرير#

احذف كود IntersectionObserver للظهور عند التمرير. احذف JavaScript لشريط تقدم التمرير. احذف معالج تمرير المنظور. animation-timeline: scroll() و animation-timeline: view() تتعامل مع كل هذا بأداء أفضل (خيط التركيب، وليس الخيط الرئيسي).

javascript
// Before: JavaScript you can delete
const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible');
        observer.unobserve(entry.target);
      }
    });
  },
  { threshold: 0.1 }
);
 
document.querySelectorAll('.reveal').forEach((el) => observer.observe(el));
css
/* After: CSS that replaces all of the above */
.reveal {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}
 
@keyframes reveal {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

Popper.js / Floating UI لتحديد المواقع#

إذا كنت تستخدم Floating UI (سابقاً Popper.js) فقط لتحديد موقع التلميحات/النوافذ المنبثقة الأساسي، فتحديد موقع المرساة في CSS يحل محلها. تفقد بعض الميزات المتقدمة (العناصر الافتراضية، وسيط مخصص)، لكن لحالة الاستخدام بنسبة 90% — "ضع هذه النافذة المنبثقة بالقرب من هذا الزر وأبقها في نافذة العرض" — CSS تفعلها أصلاً الآن.

حيل التوسيط بـ margin: auto#

هذا ليس جديداً، لكنني لا أزال أرى margin: 0 auto في كل مكان. أدوات التخطيط الحديثة تجعلها غير ضرورية في معظم الحالات:

css
/* Old way */
.container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 1rem;
}
 
/* Better: CSS Grid or Flexbox */
.page {
  display: grid;
  place-items: center;
}
 
/* Or: container queries make the container itself responsive */
.content {
  container-type: inline-size;
  width: min(100% - 2rem, 1200px);
  margin-inline: auto; /* OK, still auto, but margin-inline is clearer */
}

margin: 0 auto لا يزال يعمل جيداً. لكن إذا وجدت نفسك تستخدمها للتوسيط العمودي أو المحاذاة المعقدة، استخدم flexbox أو grid بدلاً من ذلك.

استعلامات الوسائط للاستجابة على مستوى المكوّن#

هذه هي الكبيرة. إذا كنت تكتب استعلامات @media لجعل مكوّن متجاوباً، فغالباً تفعل ذلك بشكل خاطئ الآن. استعلامات الحاوية يجب أن تكون خيارك الافتراضي للاستجابة على مستوى المكوّن. احتفظ باستعلامات @media لقرارات مستوى الصفحة: تغييرات التخطيط، أنماط التنقل، أنماط الطباعة.

css
/* Before: Media query for component styling (wrong scope) */
@media (min-width: 768px) {
  .product-card {
    flex-direction: row;
  }
}
 
/* After: Container query (right scope) */
.product-card-wrapper {
  container-type: inline-size;
}
 
@container (min-width: 400px) {
  .product-card {
    flex-direction: row;
  }
}

الصورة الأكبر#

هذه الميزات لا توجد بمعزل. إنها تتركّب. استعلامات الحاوية + :has() + التداخل + oklch() + الطبقات — مستخدمة معاً — تمنحك تجربة كتابة CSS كانت ستكون غير معروفة قبل خمس سنوات:

css
@layer components {
  .card-wrapper {
    container-type: inline-size;
  }
 
  .card {
    --accent: oklch(55% 0.2 260);
 
    display: grid;
    gap: 1rem;
    padding: 1.5rem;
    border-radius: 0.5rem;
    background: white;
    border: 1px solid oklch(90% 0 0);
 
    &:has(img) {
      padding-top: 0;
 
      img {
        border-radius: 0.5rem 0.5rem 0 0;
        width: 100%;
        aspect-ratio: 16 / 9;
        object-fit: cover;
      }
    }
 
    &:has(.badge.new) {
      border-color: var(--accent);
    }
 
    .title {
      font-size: 1.125rem;
      font-weight: 600;
      color: oklch(20% 0 0);
      text-wrap: balance;
    }
 
    .meta {
      font-size: 0.875rem;
      color: oklch(50% 0 0);
    }
 
    @container (min-width: 500px) {
      grid-template-columns: 200px 1fr;
 
      &:has(img) {
        padding: 0;
 
        img {
          border-radius: 0.5rem 0 0 0.5rem;
          height: 100%;
          aspect-ratio: auto;
        }
      }
 
      .content {
        padding: 1.5rem;
      }
    }
  }
}

تلك الكتلة الواحدة تتعامل مع:

  • تخطيط متجاوب على مستوى المكوّن (استعلامات الحاوية)
  • تنسيق شرطي بناءً على المحتوى (:has())
  • تنظيم نظيف للمحدّدات (التداخل)
  • ألوان موحّدة إدراكياً (oklch)
  • إدارة خصوصية متوقعة (@layer)

لا معالج أولي. لا وقت تشغيل CSS-in-JS. لا أصناف مساعدة لهذا. فقط CSS، تفعل ما كان يجب أن تفعله CSS طوال الوقت.

المنصة لحقت بنا. أخيراً. وكان الانتظار يستحق.

مقالات ذات صلة