: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 ישיר. כיוון התמונה לא נכון! 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() כדי להציג ולהסתיר את הודעת השגיאה בשדה. ניקח את קבוצת השדות 'email' ונוסיף לה הודעת שגיאה.

<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.

זוהי מחשבה מעניינת, אבל האם היא מובילה אותנו לסימני markup נקיים יותר ולפחות קוד? פחות 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.