:has(): محدد العائلة

منذ بدء الوقت (بمصطلحات CSS)، عملنا مع تسلسل بطرق مختلفة. تُشكّل أنماطنا "صفحة أنماط متتالية". وتتسلسل أدوات الاختيار أيضًا. ويمكن أن تتجه إلى الجانب. وفي معظم الحالات، ينخفض السعر. ولكن لا يتم أبدًا رفع السعر. لطالما حلمنا بتوفير "أداة اختيار الوالدَين". وقد أصبح متاحًا الآن. على شكل محدِّد صوري :has().

تمثّل الفئة الزائفة :has() في CSS عنصرًا إذا كان أيّ من أدوات الاختيار التي تم تمريرها كمَعلمات تتطابق مع عنصر واحد على الأقل.

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

توافق المتصفّح

قبل المتابعة، ننصحك بالاطّلاع على المتصفحات المتوافقة. لم يتم الانتهاء من ذلك بعد. ولكنّه أصبح قريبًا. لا تتوفّر هذه الميزة في Firefox بعد، ولكننا نعمل على توفيرها. ولكن هذه الميزة متوفّرة حاليًا في Safari ومن المقرر طرحها في الإصدار 105 من Chromium. ستُعلمك جميع العروض التوضيحية الواردة في هذه المقالة إذا لم تكن متوافقة مع المتصفّح المستخدَم.

كيفية استخدام ‎ :has

ما هو شكلها؟ راجِع رمز HTML التالي الذي يتضمّن عنصرَين أخويَين يحملان الفئة everybody. كيف يمكنك اختيار العنصر الذي يحتوي على سلف من الفئة a-good-time؟

<div class="everybody">
  <div>
    <div class="a-good-time"></div>
  </div>
</div>

<div class="everybody"></div>

باستخدام :has()، يمكنك إجراء ذلك باستخدام CSS التالي.

.everybody:has(.a-good-time) {
  animation: party 21600s forwards;
}

يؤدي ذلك إلى اختيار أول مثيل من .everybody وتطبيق animation.

في هذا المثال، العنصر الذي يحمل الفئة everybody هو الهدف. الشرط هو أن يكون هناك سلف ينتمي إلى الفئة a-good-time.

<target>:has(<condition>) { <styles> }

ولكن يمكنك تحقيق المزيد من ذلك لأنّ :has() توفّر الكثير من الفرص. حتى تلك التي لم يتم اكتشافها بعد. ننصحك بالاطّلاع على بعض هذه النقاط.

اختَر عناصر figure التي تحتوي على figcaption مباشر. css figure:has(> figcaption) { ... } اختَر anchor التي لا تحتوي على عنصر SVG فرعي مباشر css a:not(:has(> svg)) { ... } اختَر label التي تحتوي على عنصر input شقيق مباشر. Going sideways! css label:has(+ input) { … } اختَر article حيث لا يتضمّن العنصر الفرعي img نص alt css article:has(img:not([alt])) { … } اختَر documentElement حيث تتوفّر بعض الحالات في DOM css :root:has(.menu-toggle[aria-pressed=”true”]) { … } اختَر حاوية التنسيق التي تحتوي على عدد فردي من العناصر الفرعية css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } اختَر كل العناصر في شبكة لم يتم تمرير مؤشر الماوس فوقها css .grid:has(.grid__item:hover) .grid__item:not(:hover) { ... } اختَر الحاوية التي تحتوي على عنصر مخصّص <todo-list> css main:has(todo-list) { ... } اختَر كل a فردي ضمن فقرة تحتوي على عنصر hr شقيق مباشر css p:has(+ hr) a:only-child { … } اختَر article حيث يتم استيفاء شروط متعدّدة css article:has(>h1):has(>h2) { … } يمكنك مزج هذه العناصر مع بعضها. اختَر article حيث يلي العنوان عنوان فرعي css article:has(> h1 + h2) { … } اختَر :root عند بدء الحالات التفاعلية css :root:has(a:hover) { … } اختَر الفقرة التي تلي figure التي لا تحتوي على figcaption css figure:not(:has(figcaption)) + p { … }

ما هي حالات الاستخدام المثيرة للاهتمام التي يمكن أن تخطر لك بشأن :has()؟ والأمر المثير للاهتمام هنا هو أنّه يشجّعك على كسر النموذج الذهني. ويدفعك إلى التفكير في أنّه "هل يمكنني التعامل مع هذه الأنماط بطريقة مختلفة؟".

أمثلة

لنطّلِع على بعض الأمثلة على كيفية استخدامها.

البطاقات

اطّلِع على عرض توضيحي لبطاقة كلاسيكية. يمكننا عرض أي معلومات في بطاقتنا، مثل عنوان أو عنوان فرعي أو بعض الوسائط. في ما يلي البطاقة الأساسية.

<li class="card">
  <h2 class="card__title">
      <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
</li>

ماذا يحدث عندما تريد إدراج بعض الوسائط؟ في هذا التصميم، يمكن تقسيم البطاقة إلى عمودَين. في السابق، كان بإمكانك إنشاء فئة جديدة لتمثيل هذا السلوك، على سبيل المثال card--with-media أو card--two-columns. لا يصعب فقط تذكُّر أسماء الفئات هذه، بل يصعب أيضًا الحفاظ عليها وتذكُّرها.

باستخدام :has()، يمكنك رصد أنّ البطاقة تحتوي على بعض الوسائط واتّخاذ الإجراء المناسب. ليست هناك حاجة إلى أسماء فئات المُعدِّلات.

<li class="card">
  <h2 class="card__title">
    <a href="/article.html">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
</li>

وليس عليك تركها على هذا النحو. يمكنك استخدامها بطريقة إبداعية. كيف يمكن أن تتوافق البطاقة التي تعرض محتوى "مميّزًا" مع تنسيق معيّن؟ ستؤدي هذه القيمة إلى جعل البطاقة المميّزة بالعرض الكامل للتخطيط ووضعها في بداية الشبكة.

.card:has(.card__banner) {
  grid-row: 1;
  grid-column: 1 / -1;
  max-inline-size: 100%;
  grid-template-columns: 1fr 1fr;
  border-left-width: var(--size-4);
}

ماذا لو كانت البطاقة المميّزة التي تتضمّن بانرًا تهتز لجذب الانتباه؟

<li class="card">
  <h2 class="card__title">
    <a href="#">Some Awesome Article</a>
  </h2>
  <p class="card__blurb">Here's a description for this awesome article.</p>
  <small class="card__author">Chrome DevRel</small>
  <img
    class="card__media"
    alt=""
    width="400"
    height="400"
    src="./team-awesome.png"
  />
  <div class="card__banner"></div>
</li>

.card:has(.card__banner) {
  --color: var(--green-3-hsl);
  animation: wiggle 6s infinite;
}

هناك العديد من الخيارات.

النماذج

ماذا عن النماذج؟ ويُعرف أنّه من الصعب تنسيقها. ومن الأمثلة على ذلك تنسيق الإدخالات وتصنيفاتها. كيف نشير إلى أنّ الحقل صالح على سبيل المثال؟ باستخدام :has()، يصبح هذا أسهل بكثير. يمكننا الربط بفئات النموذج الزائفة ذات الصلة، مثل :valid و:invalid.

<div class="form-group">
  <label for="email" class="form-label">Email</label>
  <input
    required
    type="email"
    id="email"
    class="form-input"
    title="Enter valid email address"
    placeholder="Enter valid email address"
  />   
</div>
label {
  color: var(--color);
}
input {
  border: 4px solid var(--color);
}

.form-group:has(:invalid) {
  --color: var(--invalid);
}

.form-group:has(:focus) {
  --color: var(--focus);
}

.form-group:has(:valid) {
  --color: var(--valid);
}

.form-group:has(:placeholder-shown) {
  --color: var(--blur);
}

يمكنك تجربة ذلك في هذا المثال: حاوِل إدخال قيم صالحة وغير صالحة وإزالة التركيز وإعادته.

يمكنك أيضًا استخدام :has() لعرض رسالة الخطأ وإخفائها في حقل معيّن. خذ مجموعة حقول "البريد الإلكتروني" وأضِف رسالة خطأ إليها.

<div class="form-group">
  <label for="email" class="form-label">
    Email
  </label>
  <div class="form-group__input">
    <input
      required
      type="email"
      id="email"
      class="form-input"
      title="Enter valid email address"
      placeholder="Enter valid email address"
    />   
    <div class="form-group__error">Enter a valid email address</div>
  </div>
</div>

يتم تلقائيًا إخفاء رسالة الخطأ.

.form-group__error {
  display: none;
}

ولكن عندما يصبح الحقل :invalid ولا يتم التركيز عليه، يمكنك عرض الرسالة بدون الحاجة إلى أسماء فئات إضافية.

.form-group:has(:invalid:not(:focus)) .form-group__error {
  display: block;
}

لا داعي للامتناع عن إضافة لمسة من الخيال عندما يتفاعل المستخدمون مع النموذج. إليك مثال على ذلك. راقِب ما يحدث عند إدخال قيمة صالحة للتفاعل الصغير. ستؤدي قيمة :invalid إلى اهتزاز مجموعة النماذج. ولكن هذا يحدث فقط إذا لم يكن لدى المستخدم إعدادات مفضّلة للحركة.

المحتوى

لقد تطرقنا إلى ذلك في أمثلة الرموز البرمجية. ولكن كيف يمكنك استخدام :has() في مسار المستند؟ ويقدّم أفكارًا حول كيفية تصميم الخط حول الوسائط مثلاً.

figure:not(:has(figcaption)) {
  float: left;
  margin: var(--size-fluid-2) var(--size-fluid-2) var(--size-fluid-2) 0;
}

figure:has(figcaption) {
  width: 100%;
  margin: var(--size-fluid-4) 0;
}

figure:has(figcaption) img {
  width: 100%;
}

يحتوي هذا المثال على أرقام. عندما لا يكون هناك figcaption، تطفو داخل المحتوى. عندما يكون هناك figcaption، يشغل العنصر العرض الكامل ويحصل على هامش إضافي.

التفاعل مع الحالة

ماذا لو جعلت أنماطك تستجيب لبعض الحالات في الترميز؟ إليك مثال على شريط التنقّل "الكلاسيكي" المنزلق. إذا كان لديك زرّ يتيح فتح شريط التنقّل وإغلاقه، قد يستخدم السمة aria-expanded. يمكن استخدام JavaScript لتعديل السمات المناسبة. عندما يكون aria-expanded هو true، استخدِم :has() لرصد ذلك وتعديل أنماط شريط التنقّل المنزلق. تؤدي JavaScript دورها ويمكن لـ CSS تنفيذ ما تريده بهذه المعلومات. لا حاجة إلى ترتيب الترميز أو إضافة أسماء فئات إضافية وما إلى ذلك (ملاحظة: هذا ليس مثالاً جاهزًا للنشر).

:root:has([aria-expanded="true"]) {
    --open: 1;
}
body {
    transform: translateX(calc(var(--open, 0) * -200px));
}

هل يمكن أن يساعد :has في تجنُّب أخطاء المستخدمين؟

ما هو القاسم المشترك بين كل هذه الأمثلة؟ بالإضافة إلى أنّها تعرض طرقًا لاستخدام :has()، لم تتطلّب أيّ منها تعديل أسماء الفئات. وأدخَل كلّ منهما محتوى جديدًا وعدّل سمة. هذه ميزة رائعة في :has()، إذ يمكن أن تساعد في تقليل الأخطاء التي يرتكبها المستخدمون. باستخدام :has()، يمكن لـ CSS تولي مسؤولية التكيّف مع التعديلات في DOM. لست بحاجة إلى التلاعب بأسماء الفئات في JavaScript، ما يقلل من احتمالية حدوث خطأ من المطوّر. لقد حدث لنا جميعًا أن كتبنا اسم فئة بشكل خاطئ واضطررنا إلى الاحتفاظ به في عمليات البحث Object.

هذه فكرة مثيرة للاهتمام، ولكن هل تؤدي إلى استخدام ترميز أكثر وضوحًا ورمز أقل؟ استخدام كمية أقل من JavaScript لأنّنا لا نُجري العديد من التعديلات على JavaScript استخدام علامات HTML أقلّ لأنّك لم تعُد بحاجة إلى فئات مثل card card--has-media وما إلى ذلك

التفكير خارج الصندوق

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

<div class="step">
  <label for="step--1">1</label>
  <input id="step--1" type="checkbox" />
</div>
<div class="step">
  <label for="step--2">2</label>
  <input id="step--2" type="checkbox" />
</div>
.step:has(:checked), .step:first-of-type:has(:checked) {
  --hue: 10;
  opacity: 0.2;
}


.step:has(:checked) + .step:not(.step:has(:checked)) {
  --hue: 210;
  opacity: 1;
}

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

وماذا عن لعبة سلك التوتر الكلاسيكية؟ من الأسهل إنشاء آلية المكافآت باستخدام :has(). إذا مرّرت مؤشر الماوس فوق السلك، تنتهي اللعبة. نعم، يمكننا إنشاء بعض آليات اللعب هذه باستخدام عناصر مثل العناصر المُدمجة للعناصر الشقيقة (+ و~). ولكنّ :has() هو طريقة لتحقيق النتائج نفسها بدون الحاجة إلى استخدام "حيل" ترميز مثيرة للاهتمام. يُرجى العِلم أنّه من الأفضل مشاهدة هذا العرض الترويجي في علامة تبويب متصفّح منفصلة.

على الرغم من أنّك لن تستخدم هذه العناصر في مرحلة الإنتاج قريبًا، إلا أنّها تُبرز الطرق التي يمكنك من خلالها استخدام العنصر الأساسي. مثل إمكانية ربط :has().

:root:has(#start:checked):has(.game__success:hover, .screen--win:hover)
.screen--win {
  --display-win: 1;
}

الأداء والقيود

قبل إنهاء المحادثة، ما هي الإجراءات التي لا يمكنك تنفيذها باستخدام :has()؟ هناك بعض القيود المفروضة على :has(). تحدث المشاكل الرئيسية بسبب انخفاض الأداء.

  • لا يمكنك :has() :has(). ولكن يمكنك ربط :has(). css :has(.a:has(.b)) { … }
  • عدم استخدام عنصر صوري في :has() css :has(::after) { … } :has(::first-letter) { … }
  • حصر استخدام :has() داخل العناصر الزائفة التي تقبل فقط المحدّدات المركبة css ::slotted(:has(.a)) { … } :host(:has(.a)) { … } :host-context(:has(.a)) { … } ::cue(:has(.a)) { … }
  • حصر استخدام :has() بعد العنصر الزائف css ::part(foo):has(:focus) { … }
  • استخدام :visited سيكون دائمًا خطأ css :has(:visited) { … }

للاطّلاع على مقاييس الأداء الفعلية ذات الصلة بـ :has()، يمكنك الاطّلاع على هذا التأثير. نشكر "بيونغوو" على مشاركة هذه الإحصاءات والتفاصيل حول التنفيذ.

هذا كل ما في الأمر.

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

تتوفّر جميع العروض التوضيحية في مجموعة CodePen هذه.