בתוך ה-polyfill של שאילתת הקונטיינר

Gerald Monaco
Gerald Monaco

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

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

מאחורי הקלעים

טרנספיליציה

כשמנתח ה-CSS בדפדפן נתקל בכלל at-rule לא ידוע, כמו הכלל החדש @container, הוא יטמין אותו כאילו הוא אף פעם לא היה קיים. לכן, הדבר הראשון והחשוב ביותר ש-polyfill צריך לעשות הוא להמיר שאילתת @container למשהו שלא יושלך.

השלב הראשון בתרגום הוא המרת הכלל @container ברמת העליונה לשאילתה @media. הדרישה הזו מבטיחה שהתוכן יישאר מקובץ. לדוגמה, כשמשתמשים בממשקי ה-API של CSSOM וכשמעיינים במקור ה-CSS.

לפני
@container (width > 300px) {
  /* content */
}
אחרי
@media all {
  /* content */
}

לפני שאילתות מאגרי תגים, ל-CSS לא הייתה דרך להפעיל או להשבית קבוצות כללים באופן שרירותי. כדי לבצע פוליגונימיזציה של ההתנהגות הזו, צריך גם לשנות את הכללים שבתוך שאילתה של מאגר. לכל @container מוקצה מזהה ייחודי משלו (לדוגמה, 123), שמשמש להמרת כל בורר כך שיחול רק אם לאלמנט יש מאפיין cq-XYZ שכולל את המזהה הזה. המאפיין הזה יוגדר על ידי ה-polyfill בזמן הריצה.

לפני
@container (width > 300px) {
  .card {
    /* ... */
  }
}
אחרי
@media all {
  .card:where([cq-XYZ~="123"]) {
    /* ... */
  }
}

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

@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}

בהינתן שירות ה-CSS הזה, ברכיב עם המחלקה .card צריך תמיד להיות color: red, כי הכלל המאוחר יותר תמיד יחליף את הכלל הקודם עם אותם סלקטור ואותו ספציפיות ספציפיות. לכן, אם מחליפים את הכלל הראשון וכוללים בורר מאפיינים נוסף ללא :where(...), רמת הדיוק של הנתונים עשויה להיות גבוהה יותר, וכתוצאה מכך color: blue מופעל בטעות.

עם זאת, פסאודו-הקלאס :where(...) הוא יחסית חדש. בדפדפנים שלא תומכים באפשרות הזו, ה-polyfill מספק פתרון בטוח וקל לעקיפת הבעיה: אפשר בכוונה להגביר את מידת הספציפיות של הכללים על ידי הוספה ידנית של בורר :not(.container-query-polyfill) מדומה לכללי @container:

לפני
@container (width > 300px) {
  .card {
    color: blue;
  }
}

.card {
  color: red;
}
אחרי
@container (width > 300px) {
  .card:not(.container-query-polyfill) {
    color: blue;
  }
}

.card {
  color: red;
}

יש לכך כמה יתרונות:

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

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

פסאודו אלמנטים

שאלה אחת שאתם עשויים לשאול את עצמכם היא: אם ה-polyfill מגדיר מאפיין cq-XYZ ברכיב כך שיכלול את מזהה המאגר הייחודי 123, איך אפשר לתמוך ברכיבים מדומה, שלא ניתן להגדיר בהם מאפיינים?

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

לפני
@container (width > 300px) {
  #foo::before {
    /* ... */
  }
}
אחרי
@media all {
  #foo:where([cq-XYZ~="123"])::before {
    /* ... */
  }
}

במקום להפוך ל-#foo::before:where([cq-XYZ~="123"]) (שלא תקין), הבורר המותנה מועבר לסוף הרכיב המקורי, #foo.

עם זאת, זה לא הכול. קונטיינר לא רשאי לשנות שום דבר שלא כלול בתוכו (וקונטיינר לא יכול להיות בתוך עצמו), אבל חשוב לזכור שזה מה שקורה בדיוק אם #foo היה רכיב הקונטיינר עצמו שלגביו נשלחת שאילתה. המאפיין #foo[cq-XYZ] ישתנה בטעות, וכל הכללים של #foo יוחלו בטעות.

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

לפני
@container (width > 300px) {
  #foo,
  #foo::before {
    /* ... */
  }
}
אחרי
@media all {
  #foo:where([cq-XYZ-A~="123"]),
  #foo:where([cq-XYZ-B~="123"])::before {
    /* ... */
  }
}

מכיוון שמאגר תגים אף פעם לא יחיל את המאפיין הראשון (cq-XYZ-A) על עצמו, הבורר הראשון יתאים רק אם מאגר הורה שונה עמד בתנאי המאגר והחיל אותו.

יחידות יחסיות של מאגר

שאילתות של מאגרים כוללות גם כמה יחידות חדשות שאפשר להשתמש בהן ב-CSS, כמו cqw ו-cqh עבור 1% מהרוחב והגובה (בהתאמה) של מאגר האב הקרוב והמתאים ביותר. כדי לתמוך באפשרויות האלה, היחידה מומרת לביטוי calc(...) באמצעות מאפיינים מותאמים אישית של CSS. ה-polyfill יגדיר את הערכים של המאפיינים האלה באמצעות סגנונות מוטבעים ברכיב הקונטיינר.

לפני
.card {
  width: 10cqw;
  height: 10cqh;
}
אחרי
.card {
  width: calc(10 * --cq-XYZ-cqw);
  height: calc(10 * --cq-XYZ-cqh);
}

יש גם יחידות לוגיות, כמו cqi ו-cqb, לציון גודל בתוך השורה וגודל בלוק (בהתאמה). הדברים האלה קצת יותר מורכבים, כי צירי השורה והבלוק נקבעים לפי writing-mode של הרכיב שמשתמש ביחידת המידה, ולא של הרכיב שאליו מופנית השאילתה. כדי לתמוך בכך, ה-polyfill מחיל סגנון מוטמע על כל רכיב ש-writing-mode שלו שונה מזה של ההורה שלו.

/* Element with a horizontal writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqw);
--cq-XYZ-cqb: var(--cq-XYZ-cqh);

/* Element with a vertical writing mode */
--cq-XYZ-cqi: var(--cq-XYZ-cqh);
--cq-XYZ-cqb: var(--cq-XYZ-cqw);

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

מאפיינים

שאילתות של קונטיינרים מוסיפות גם כמה מאפייני CSS חדשים, כמו container-type ו-container-name. מאחר שאי אפשר להשתמש בממשקי API כמו getComputedStyle(...) עם מאפיינים לא ידועים או לא חוקיים, גם הם עוברים טרנספורמציה למאפיינים מותאמים אישית של CSS אחרי הניתוח. אם לא ניתן לנתח נכס (למשל כי הוא מכיל ערך לא חוקי או לא ידוע), הוא פשוט נשאר לבד כדי שהדפדפן יוכל לטפל בו.

לפני
.card {
  container-name: card-container;
  container-type: inline-size;
}
אחרי
.card {
  --cq-XYZ-container-name: card-container;
  --cq-XYZ-container-type: inline-size;
}

המאפיינים האלה עוברים טרנספורמציה בכל פעם שהם מתגלים, וכך ה-polyfill יכול לפעול בצורה חלקה עם תכונות CSS אחרות כמו @supports. הפונקציונליות הזו היא הבסיס לשיטות המומלצות לשימוש ב-polyfill, כפי שמתואר בהמשך.

לפני
@supports (container-type: inline-size) {
  /* ... */
}
אחרי
@supports (--cq-XYZ-container-type: inline-size) {
  /* ... */
}

כברירת מחדל, מאפיינים מותאמים אישית של CSS עוברים בירושה. לדוגמה, כל צאצא של .card יקבל את הערך של --cq-XYZ-container-name ושל --cq-XYZ-container-type. זה בהחלט לא האופן שבו הנכסים המקוריים פועלים. כדי לפתור זאת, ה-polyfill יוסיף את הכלל הבא לפני כל סגנונות משתמש, ויבטיח שכל רכיב מקבל את הערכים הראשוניים, אלא אם כן כלל אחר יבטל באופן מכוון את הערכים.

* {
  --cq-XYZ-container-name: none;
  --cq-XYZ-container-type: normal;
}

שיטות מומלצות

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

במהלך הטעינה הראשונית, יש הרבה דברים שצריכים לקרות לפני ש-polyfill יכול לבצע את הפריסה של הדף:

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

אם הבעיות האלה לא יטופלו בקפידה על ידי ה-polyfill, ייתכן שהמדדים הבסיסיים של חוויית המשתמש (Core Web Vitals) שלכם יירדו.

כדי להקל עליכם לספק למבקרים חוויה נעימה, ה-polyfill תוכנן לתת עדיפות להשהיה לאחר קלט ראשוני (FID) ול-Cumulative Layout Shift ‏ (CLS), על חשבון הצגת התוכן הכי גדול ‏ (LCP). באופן ספציפי, ה-polyfill לא מבטיח שהשאילתות של הקונטיינר ייבדקו לפני הצגת התמונה הראשונית. כלומר, כדי לספק את חוויית המשתמש הטובה ביותר, חובה לוודא שכל תוכן שהגודל או המיקום שלו יושפעו משימוש בשאילתות מאגר יהיה מוסתר עד שה-polyfill יטמיע ויתרגם את ה-CSS. אחת מהדרכים לעשות זאת היא באמצעות כלל @supports:

@supports not (container-type: inline-size) {
  #content {
    visibility: hidden;
  }
}

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

מומלץ להשתמש בגישה הזו מכמה סיבות:

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

סיכום

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

אנחנו כבר ממש סקרניים לראות את הדברים המדהימים שתיצרו בעזרתו.