: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) { … } יש לבחור את ה-articles שבהם לצאצא 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) { ... } יש לבחור כל סולו css main:has(todo-list) { ... } יש לבחור כל סולו css main:has(todo-list) { ... } יש לבחור כל סולו css main:has(todo-list) { ... } שבו יש {10/1} תנאי סולו css main:has(todo-list) { ... } בתוך פסקה אחת שיש בה כמה css main:has(todo-list) { ... } פסקה שיש בה כמה תנאים {11/1} בתוך פסקה אחת שיש בה מספר צאצאים css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } בוחרים את כל הפריטים ברשת עם מספר אי-זוגי של צאצאים css .container:has(> .container__item:last-of-type:nth-of-type(odd)) { ... } בוחרים את כל הפריטים ברשת שמכילה רכיב מותאם אישית <todo-list> css main:has(todo-list) { ... } יש לבחור כל סולו css main:has(todo-list) { ... }.articleahrcss p:has(+ hr) a:only-child { … }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 יגרום לקבוצת הטפסים לרטוט. אבל רק אם למשתמש אין העדפות תנועה.

Content

נוגעים לכך בדוגמאות של הקוד. אבל, איך אפשר להשתמש ב-: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() זמינים בתקלה הזו. קרדיט ל-Byung Woo על שיתוף התובנות והפרטים האלה בנוגע להטמעה.

סיימתם!

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

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