:has(): הבורר של הקבוצה המשפחתית

מאז תחילת העבודה (במונחים של CSS), עבדנו עם דרגות שונות במובנים שונים. הסגנונות שלנו יוצרים "גיליון סגנונות מדורג". גם הסלקטורים שלנו יורדים. הם יכולים ללכת הצידה. ברוב המקרים הם יורדים. אבל אף פעם לא למעלה. במשך שנים פנטנו על 'בורר ההורים'. ועכשיו הוא סוף סוף מגיע! בצורת סלקטור פסאודו :has().

המחלקה המדומה :has() של CSS מייצגת רכיב אם אחד מהסלקטורים שמועברים כפרמטרים תואם לפחות לרכיב אחד.

אבל הם יותר מהורה זו דרך טובה לשווק אותו. הדרך הלא מושכת היא "סביבה מותנית" אבל הצלצול הזה לא בדיוק זהה לזה. מה לגבי ה'משפחה' להשתמש בבורר?

תמיכה בדפדפן

לפני שנמשיך, כדאי לדבר על תמיכה בדפדפן. עדיין לא לגמרי. אבל זה הולך ומתקרב. אין עדיין תמיכה ב-Firefox, הוא מעבר לתוכנית. עם זאת, היא כבר ב-Safari וצפויה להפצה ב-Chromium 105. בכל ההדגמות במאמר הזה תוכלו לראות אם הן לא נתמכות בדפדפן שבו אתם משתמשים.

כיצד להשתמש ב- :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. זזים הצידה! 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>

ואין צורך להשאיר אותה שם. אתם יכולים להיות יצירתיים בקטע הזה. איך כרטיס שמציג תוכן 'מוצג' יכול להתאים את עצמו לפריסה? שירות ה-CSS הזה יוצר כרטיס מוצג ברוחב מלא של הפריסה ומציב אותו בתחילת רשת.

.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(), שירות ה-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 תמיד יהיה False css :has(:visited) { … }

בתקלה אפשר למצוא מדדי ביצועים בפועל שקשורים ל-:has(). קרדיט ל-Byungwoo על שיתוף התובנות והפרטים האלה בנוגע להטמעה.

זהו!

כדאי להתכונן לקראת :has(). ספרו על כך לחברים ושתפו את הפוסט הזה. הוא ישנה את הגישה שלנו לשירות CSS.

כל ההדגמות זמינות באוסף של CodePen.