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

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

פורסם: 4 באוקטובר 2023

Browser Support

  • Chrome: 118.
  • Edge: 118.
  • Firefox: 146.
  • Safari: 17.4.

Source

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

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

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

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

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

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

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

חדש: @scope

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

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

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

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

כדי למנוע את הבחירה של רכיבי <img> בתוך אזור התוכן של הכרטיס (.card__content), אפשר להגדיר את בורר img בצורה ספציפית יותר. דרך נוספת לעשות את זה היא להשתמש בעובדה שכלל ה-at ‏@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 Nesting.

@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 וספציפיות

הסלקטורים שבהם משתמשים ב-prelude של @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 כסלקטור כדי לבצע את ההתאמה. התהליך הזה נקרא desugaring.

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

מכיוון ש-& עובר desugaring באמצעות :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>

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

‫@scope ב-cascade

בתוך CSS Cascade, ‏ @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>

כשמסתכלים על קטע קטן של תגי עיצוב, הקישור השלישי יהיה white במקום black, למרות שהוא צאצא של div עם המחלקה .light שמוחלת עליו. הסיבה לכך היא הקריטריון של סדר ההופעה, שמשמש כאן את ה-cascade כדי לקבוע את המנצח. המערכת רואה שההצהרה על .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. כשמצהירים על משתנה בתוך טווח של דונאט, המשתנה color עדיין עובר בירושה לצאצאים בתוך החור של הדונאט.

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

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