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

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

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

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

מקור

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

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

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

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

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

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

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

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

חדש: @scope

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

לדוגמה, כדי לטרגט רק את רכיבי <img> ברכיב .card, צריך להגדיר את .card בתור הרמה הבסיסית (root) של הכלל @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) { ... }

מגבלת היקף יכולה גם להפנות לרכיבים שמחוץ לרמה הבסיסית (root) שלהם באמצעות :scope. לדוגמה:

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

שימו לב שכללי הסגנון של ההיקף לא יכולים לסמן בתו בריחה (escape) את עץ המשנה. בחירות כמו :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)