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

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

תמיכה בדפדפן

  • Chrome: 118.
  • קצה: 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 לספציפי יותר. דרך נוספת לעשות זאת היא להשתמש בעובדה שהכלל @scope מקבל גם מגבלת היקף שקובעת את הגבול התחתון.

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

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

הבורר :scope

כברירת מחדל, כל כללי הסגנון ברמת ההיקף יהיו יחסיים לרמה הבסיסית (root) של ההיקף. אפשר גם לטרגט את רכיב הבסיס של ההיקף עצמו. לשם כך, צריך להשתמש בבורר :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) */
        …
    }
}

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

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

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

היקף ללא הקדמה

כשכותבים סגנונות מוטבעים עם הרכיב <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 in the cascade

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

תרשים של דירוג ה-CSS.

בהתאם למפרט:

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

השלב החדש הזה שימושי כאשר מניחים מספר וריאציות של רכיב. לדוגמה, שעדיין לא משתמשת ב-@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>

כשמציגים את קטע העיצוב הקטן הזה, הקישור השלישי יהיה 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 זהה, ולכן קריטריון הקירבה לקביעת היקף נכנס לתוקף. שני הסלקטורים משוקללים לפי הקירבה לרמה הבסיסית (root) של ההיקף שלהם. עבור רכיב a השלישי, יש רק מעבר אחד לרמה הבסיסית (root) של ההיקף .light אבל שתיים לרמה של .dark. לכן, הבורר a ב.light ינצח.

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

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

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

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

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