אפשר להגביל את פוטנציאל החשיפה של הסלקטורים בעזרת CSS @scope at-rule

איך משתמשים ב-@scope כדי לבחור רכיבים רק בתוך עץ משנה מוגבל של ה-DOM

תמיכה בדפדפנים

  • Chrome: ‏ 118.
  • Edge: ‏ 118.
  • Firefox: מאחורי דגל.
  • Safari: 17.4.

מקור

האמנות העדינה של כתיבת סלקטורים ב-CSS

כשכותבים בוחרים, יכול להיות שתמצאו את עצמכם נמצאים בין שני עולמות. מצד אחד, כדאי להיות ספציפיים לגבי הרכיבים שבוחרים. מצד שני, חשוב שעדיין יהיה קל לשנות את הסלקטורים, ושהם לא יהיו מקושרים באופן הדוק למבנה ה-DOM.

לדוגמה, אם רוצים לבחור את 'תמונת ה-Hero באזור התוכן של רכיב הכרטיס' – בחירה ספציפית למדי של רכיב – סביר להניח שלא כדאי לכתוב סלקטור כמו .card > .content > img.hero.

  • לבורר הזה יש ספציפיות גבוהה למדי של (0,3,1), ולכן קשה לשנות אותו ככל שהקוד מתארך.
  • השימוש ב-combinator של הצאצא הישיר גורם לקישור הדוק למבנה ה-DOM. אם ה-Markup ישתנה, תצטרכו לשנות גם את ה-CSS.

עם זאת, לא כדאי לכתוב רק img כסלקטור לרכיב הזה, כי הפעולה הזו תגרום לבחירה של כל רכיבי התמונות בדף.

לרוב, קשה למצוא את האיזון הנכון בנושא הזה. במשך השנים, מפתחים מסוימים הגיעו לפתרונות ולדרכים לעקיפת הבעיה שיעזרו לכם במצבים כאלה. לדוגמה:

  • שיטות כמו BEM מכתיבות לתת לאלמנט הזה את הכיתה card__img card__img--hero כדי לשמור על רמת ספציפיות נמוכה, תוך שמירה על היכולת לבחור ספציפיות.
  • פתרונות מבוססי JavaScript, כמו CSS ממוקד או רכיבים מעוצבים, כותבים מחדש את כל הסלקטורים על ידי הוספת מחרוזות שנוצרות באופן אקראי – כמו sc-596d7e0e-4 – לסלקטורים, כדי למנוע מהם לטרגט רכיבים בצד השני של הדף.
  • בחלק מהספריות הסלקטורים אפילו לא קיימים, וצריך להוסיף את הטריגרים של עיצוב הרכיבים ישירות לתווית ה-HTML.

אבל מה אם לא צריך אף אחד מהם? מה אם CSS היה נותן לכם דרך להיות ספציפיים למדי לגבי הרכיבים שבחרתם, בלי שתצטרכו לכתוב סלקטורים ספציפיים מאוד או כאלה שמקושרים בצורה הדוקה ל-DOM? כאן נכנס לתמונה ה-@scope, שמאפשר לבחור רכיבים רק בתוך עץ משנה של ה-DOM.

חדש: @scope

בעזרת @scope אפשר להגביל את פוטנציאל החשיפה של הבוררים. כדי לעשות זאת, מגדירים את שורש ההיקף, שמגדיר את הגבול העליון של עץ המשנה שרוצים לטרגט. כשמשתמשים בקבוצת שורש להגדרת היקף, כללי הסגנון הכלולים – שנקראים כללי סגנון מוגדרי היקף – יכולים לבחור רק מתוך עץ המשנה המוגבל הזה של ה-DOM.

לדוגמה, כדי לטרגט רק את הרכיבים <img> ברכיב .card, מגדירים את .card כשורש ההיקף של כלל at‏ @scope.

@scope (.card) {
    img {
        border-color: green;
    }
}

כלל הסגנון ברמת ההיקף img { … } יכול לבחור רק רכיבי <img> שנמצאים בהיקף של רכיב .card שתואם.

כדי למנוע את הבחירה של רכיבי <img> בתוך אזור התוכן של הכרטיס (.card__content), אפשר להפוך את הבורר img לספציפי יותר. דרך נוספת לעשות זאת היא להשתמש בעובדה שכלל at-rule‏ @scope מקבל גם הגבלת היקף שקובעת את הגבול התחתון.

@scope (.card) to (.card__content) {
    img {
        border-color: green;
    }
}

כלל הסגנון ברמת ההיקף הזה מטרגט רק רכיבי <img> שממוקמים בין רכיבי .card לבין רכיבי .card__content בעץ האב. סוג כזה של הגדרת היקף – עם גבול עליון ותחתון – נקרא לעיתים קרובות היקף בצורת עיגול עם חור.

הבורר :scope

כברירת מחדל, כל כללי הסגנון ברמת ההיקף הם יחסיים לשורש ההיקף. אפשר גם לטרגט את רכיב הבסיס של ההיקף עצמו. לשם כך, משתמשים בבורר :scope.

@scope (.card) {
    :scope {
        /* Selects the matched .card itself */
    }
    img {
       /* Selects img elements that are a child of .card */
    }
}

לבוררים בתוך כללי סגנון ברמת ההיקף מתווסף באופן משתמע :scope. אם רוצים, אפשר להוסיף את :scope בתחילת השם כדי לציין זאת בבירור. לחלופין, אפשר להוסיף את הסלקטור & בתחילת הקוד, באמצעות עיצוב עץ ב-CSS.

@scope (.card) {
    img {
       /* Selects img elements that are a child of .card */
    }
    :scope img {
        /* Also selects img elements that are a child of .card */
    }
    & img {
        /* Also selects img elements that are a child of .card */
    }
}

אפשר להשתמש בפסאודו-סיווג :scope במגבלה ברמת ההיקף כדי לדרוש קשר ספציפי לשורש ההיקף:

/* .content is only a limit when it is a direct child of the :scope */
@scope (.media-object) to (:scope > .content) { ... }

אפשר גם להשתמש ב-:scope כדי להפנות למגבלה ברמת ההיקף לרכיבים מחוץ לשורש ההיקף שלה. לדוגמה:

/* .content is only a limit when the :scope is inside .sidebar */
@scope (.media-object) to (.sidebar :scope .content) { ... }

חשוב לזכור שכללי הסגנון ברמת ההיקף לא יכולים לצאת מהעץ המשני. בחירות כמו :scope + p לא תקפות כי הן מנסו לבחור רכיבים שלא נכללים בהיקף.

@scope וספציפיות

הבוררים שבהם אתם משתמשים בחלק המקדים של @scope לא משפיעים על הספציפיות של הבוררים הכלולים. בדוגמה הבאה, הספציפיות של הבורר img עדיין היא (0,0,1).

@scope (#sidebar) {
    img { /* Specificity = (0,0,1) */
        …
    }
}

הספציפיות של :scope היא כמו זו של פסאודו-כיתה רגילה, כלומר (0,1,0).

@scope (#sidebar) {
    :scope img { /* Specificity = (0,1,0) + (0,0,1) = (0,1,1) */
        …
    }
}

בדוגמה הבאה, באופן פנימי, ה-& נכתב מחדש לסלקטור שמשמש לשורש ההיקף, עטוף בתוך סלקטור :is(). בסופו של דבר, הדפדפן ישתמש ב-:is(#sidebar, .card) img כסלקטור לביצוע ההתאמה. התהליך הזה נקרא הסרת סוכר.

@scope (#sidebar, .card) {
    & img { /* desugars to `:is(#sidebar, .card) img` */
        …
    }
}

מכיוון ש-& עובר הסרה של סוכר באמצעות :is(), הספציפיות של & מחושבת בהתאם לכללים של :is() לספציפיות: הספציפיות של & היא זו של הארגומנט הספציפי ביותר שלו.

בהתאם לדוגמה הזו, הספציפיות של :is(#sidebar, .card) היא זו של הארגומנט הספציפי ביותר שלה, כלומר #sidebar, ולכן היא הופכת ל-(1,0,0). משלבים את זה עם הספציפיות של img – שהיא (0,0,1) – ומקבלים את הערך (1,0,1) כספציפיות של הסלקטור המורכב כולו.

@scope (#sidebar, .card) {
    & img { /* Specificity = (1,0,0) + (0,0,1) = (1,0,1) */
        …
    }
}

ההבדל בין :scope לבין & בתוך @scope

בנוסף להבדלים באופן שבו מחושב רמת הספציפיות, יש הבדל נוסף בין :scope לבין &: הערך :scope מייצג את הבסיס של ההיקף שהותאם, ואילו הערך & מייצג את הבורר ששימש להתאמה של הבסיס של ההיקף.

לכן, אפשר להשתמש ב-& כמה פעמים. בניגוד ל-:scope, שאפשר להשתמש בו רק פעם אחת, כי אי אפשר להתאים שורש של היקף בתוך שורש של היקף.

@scope (.card) {
  & & { /* Selects a `.card` in the matched root .card */
  }
  :scope :scope { /* ❌ Does not work */
    …
  }
}

היקף ללא Prelude

כשכותבים סגנונות בתוך שורה באמצעות הרכיב <style>, אפשר להגביל את היקף הכללים של הסגנון לרכיב ההורה המקיף של רכיב <style>, על ידי כך שלא מציינים שורש להיקף. כדי לעשות זאת, משמיטים את הקדמה של @scope.

<div class="card">
  <div class="card__header">
    <style>
      @scope {
        img {
          border-color: green;
        }
      }
    </style>
    <h1>Card Title</h1>
    <img src="…" height="32" class="hero">
  </div>
  <div class="card__content">
    <p><img src="…" height="32"></p>
  </div>
</div>

בדוגמה שלמעלה, הכללים ברמת ההיקף מטרגטים רק אלמנטים בתוך div עם שם הכיתה card__header, כי div הוא רכיב ההורה של רכיב <style>.

@scope במפל

בתוך Cascade של CSS, @scope מוסיף גם קריטריון חדש: scoping proximity. השלב מופיע אחרי רמת הספציפיות אבל לפני סדר ההופעה.

תצוגה חזותית של CSS Cascade.

לפי המפרט:

כשמשווים בין הצהרות שמופיעות בכללי סגנון עם שורשי היקף שונים, ההצהרה עם הכי פחות קפיצות דור או קפיצות בין רכיבי אחים בין שורש ההיקף לבין הנושא של כלל הסגנון ברמת ההיקף מנצחת.

השלב החדש הזה שימושי כשרוצים להטמיע כמה וריאציות של רכיב. בדוגמה הבאה עדיין לא נעשה שימוש ב-@scope:

<style>
    .light { background: #ccc; }
    .dark  { background: #333; }
    .light a { color: black; }
    .dark a { color: white; }
</style>
<div class="light">
    <p><a href="#">What color am I?</a></p>
    <div class="dark">
        <p><a href="#">What about me?</a></p>
        <div class="light">
            <p><a href="#">Am I the same as the first?</a></p>
        </div>
    </div>
</div>

כשמציגים את קטע ה-Markup הזה, הקישור השלישי יהיה white במקום black, למרות שהוא צאצא של div עם הכיתה .light שחלה עליו. הסיבה לכך היא קריטריון סדר ההופעה שבו המערכת משתמשת כדי לקבוע את הזוכה. המערכת מזהה ש-.dark a הוצהר לאחרונה, ולכן הוא יזכה לפי הכלל .light a

הבעיה נפתרה עכשיו באמצעות קריטריון הקרבה ברמת ההיקף:

@scope (.light) {
    :scope { background: #ccc; }
    a { color: black;}
}

@scope (.dark) {
    :scope { background: #333; }
    a { color: white; }
}

מכיוון שלשני הבוררים ברמת ההיקף a יש את אותה רמת ספציפיות, הקריטריון של קרבה ברמת ההיקף נכנס לפעולה. הוא שוקל את שני הבוררים לפי הקרבה לשורש ההיקף שלהם. לרכיב השלישי a, יש רק קפיצה אחת לשורש ההיקף .light, אבל שתי קפיצות לשורש ההיקף .dark. לכן, הבורר a ב-.light יזכה.

הערה לסיום: בידוד של הבורר, ולא של הסגנון

חשוב לציין ש-@scope מגביל את היקף הבחירה של הבוררים, אבל הוא לא מספק בידוד של סגנונות. נכסים שעוברים בירושה לצאצאים ימשיכו לעבור בירושה, מעבר לגבול התחתון של @scope. אחד מהמאפיינים האלה הוא color. כשמגדירים את ה-one בתוך היקף של עוגת סופגנייה, ה-color עדיין יורש לצאצאים בתוך החור של העוגה.

@scope (.card) to (.card__content) {
  :scope {
    color: hotpink;
  }
}

בדוגמה שלמעלה, לרכיב .card__content ולצאצאיו יש צבע hotpink כי הם יורשים את הערך מ-.card.

(תמונת השער של rustam burkhanov ב-Unsplash)