CSS @layer: איך cascade layers מסיימים את מלחמת הספציפיות
CSS @layer: איך cascade layers מסיימים את מלחמת הספציפיות
יש לכם רכיב כרטיסייה מספריית design system: .card .card__header .card__title { font-size: 18px; } – שלוש מחלקות בסלקטור, כי כך קינן אותו מחבר הרכיב. אתם רוצים לגבור עליו במחלקת עזר אחת, .text-lg { font-size: 20px; }, שנוספה ב-HTML אחרי המחלקות של הכרטיסייה. זה לא עובד. הכותרת נשארת בעקשנות על 18px, למרות ש-.text-lg בקוד מופיע מאוחר יותר, כך שאינטואיטיבית "היה אמור לנצח החדש יותר". זו לא תקלה בדפדפן – שלוש מחלקות בסלקטור מנצחות מחלקה אחת, לא משנה מה סדר הופעתן בקובץ. כך פועלת הספציפיות מאז 1996, וכך היא אמורה לפעול.
הפתרון הקלאסי היה להעלות את הספציפיות של הכלל שלכם – .card .text-lg, או, כשזה לא מספיק, !important. שני הפתרונות עובדים נקודתית ונשברים באופן מערכתי: בכל פעם שמישהו בצוות מוסיף כלל עם ספציפיות עוד יותר גבוהה (או עוד !important), כל שאר הגיליון הופך לבלתי צפוי. זו בדיוק הבעיה הזו – לא אסתטית, אלא ארכיטקטונית – ש-CSS Cascade Layers פותרים.
ציר חדש להכרעת קונפליקטים, לא עוד דרך להעלות עדיפות
הדבר המרכזי שצריך להבין: @layer הוא לא עוד כלי להעלאת עדיפות של כלל, כמו !important או הוספת עוד מחלקה לסלקטור. זהו שלב נפרד באלגוריתם ה-cascade, שמוכרע לפני הספציפיות, לא לצדה. הסדר המלא שבו הדפדפן מכריע קונפליקט בין שני כללי CSS נראה כך (בפישוט, תוך השמטת ה-origin):
- שכבה (
@layer) – כלל משכבה שהוכרזה מאוחר יותר מנצח כלל משכבה שהוכרזה קודם, לא משנה מה הספציפיות של הסלקטורים בפנים. - ספציפיות – מוכרעת רק כאשר שני הכללים נמצאים באותה שכבה (או ששניהם כלל לא בשום שכבה).
- סדר במקור – השופט האחרון, כשהקריטריונים הקודמים מסתיימים בתיקו.
זה גורם למחלקה אחת, .text-lg, בשכבה מאוחרת יותר, להיות מסוגלת לנצח סלקטור עם שלוש מחלקות בשכבה מוקדמת יותר – כי ההתמודדות בכלל לא מגיעה לשלב של השוואת ספציפיות. השכבה מכריעה את העניין קודם.
css
/* את סדר השכבות מצהירים פעם אחת, מראש – הוא לא חייב להתאיםלסדר שבו התוכן שלהן באמת מופיע בקבצים */@layer reset, base, components, utilities;@layer components {.card .card__header .card__title {font-size: 18px;}}@layer utilities {.text-lg {font-size: 20px;}}
ההצהרה הריקה הזו, @layer reset, base, components, utilities;, בראש הקובץ, היא לא פורמליות – זו קביעה מפורשת של סדר העדיפויות, עוד לפני ששכבה כלשהי מקבלת תוכן אמיתי. בזכות זה, סדר השכבות בלתי תלוי בסדר הייבוא או בקבצים – components יכול פיזית להימצא בקוד לפני utilities, ובכל זאת utilities ינצח, כי כך הוכרז הסדר בהתחלה.
דוגמה מעשית: reset, רכיבים ומחלקות עזר שלעולם לא נלחמים זה בזה
בפועל, הדפוס הזה – reset נמוך יותר מרכיבים, רכיבים נמוכים יותר ממחלקות עזר – הוא בדיוק איך שפועלת למשל מערכת השכבות ב-Tailwind מגרסה 3.1: מחלקת עזר אמורה תמיד לנצח כלל של רכיב, לא משנה כמה מחלקות יש לסלקטור של אותו רכיב, כי בשביל זה בדיוק מחלקות עזר בכלל קיימות.
css
@layer reset, base, components, utilities;@layer reset {* {margin: 0;padding: 0;box-sizing: border-box;}}@layer base {body {font-family: system-ui, sans-serif;color: #1a1a1a;}}@layer components {.card {border: 1px solid #e0e0e0;border-radius: 12px;padding: 16px;}.card .card__header .card__title {font-size: 18px;font-weight: 600;}}@layer utilities {.text-lg {font-size: 20px !important;}.mt-4 {margin-top: 16px;}}
שימו לב שבשכבת utilities ה-!important ליד .text-lg מיותר כאן – השכבה כבר מבטיחה עדיפות, בלי צורך לפנות לכלי הכבד ביותר ב-CSS. זו המחשה טובה לכך ש-cascade layers באמת מצמצמים את כמות ה-!important בקוד: הוא מפסיק להיות דרוש כדרך לעקוף ספציפיות, כי יש לכם כלי שתוכנן בדיוק למשימה הזו.
אותו מנגנון פותר בעיה שנייה, נפוצה באותה מידה – ספריית CSS חיצונית, שאת הסלקטורים שלה אינכם שולטים בהם, ושאת העדיפות שלה אתם רוצים תמיד נמוכה מהסגנונות שלכם, בלי להעלות את הספציפיות של הכללים שלכם:
css
@import url("some-ui-library.css") layer(vendor);@layer vendor, base, components, utilities;
כל הספרייה עוברת לשכבת vendor, הממוקמת בתחילת סדר העדיפויות – כך ש-base, components ו-utilities שלכם תמיד ינצחו אותה, לא משנה כמה אגרסיבית הספרייה הזו בונה את הסלקטורים שלה.
מלכודת ששוברת אינטואיציה: סגנון בלי שכבה מנצח את כל השכבות
זה הפרט הכי הרבה פעמים מוחמץ, ובד בבד הכי משמעותי מכל המנגנון: כלל שלא נמצא בשום שכבה מנצח כלל בכל שכבה שהיא – עבור הצהרות רגילות, לא עם !important. הוא לא "מפסיד כי הוא בשכבה בלתי-נראית בסוף" – הוא מנצח תמיד, ללא תנאי, כל כלל בשכבה, אפילו אם השכבה ההיא הוכרזה כבעלת העדיפות הגבוהה ביותר.
css
@layer utilities {.text-lg {font-size: 20px;}}/* הכלל הזה, למרות שהוא לא בשום שכבה ונראה כמו overrideאקראי, בודד – מנצח את .text-lg למעלה,לא משנה איפה בקובץ הוא נמצא */.title {font-size: 16px;}
ההתנהגות הזו מכוונת – בזכותה, אימוץ הדרגתי של cascade layers בפרויקט גדול וקיים בטוח: כל ה-CSS ה"ישן", שעדיין לא הגיע לשום שכבה, שומר כברירת מחדל על עדיפות עליונה, כך שהוא לא מתחיל פתאום להפסיד לשכבות שנוספו לאחרונה. אבל אותה התנהגות בדיוק היא מקור הבאגים המבלבלים ביותר בצוותים שמאמצים @layer בצורה חלקית: כלל בודד שנשאר בטעות מחוץ לשכבה – שגיאת הקלדה, ייבוא ששכחו, בלוק <style> שהוזרק על ידי ספרייה חיצונית – ינצח את כל מערכת העדיפויות המתוכננת בקפידה, בלי שום אזהרה. המסקנה המעשית: אם אתם בוחרים ב-cascade layers, שימו הכול בעקביות באיזושהי שכבה, כולל reset וסגנונות בסיס – אל תשאירו שום כלל "בחוץ", כי הוא, לא שכבת utilities, ינצח כל קונפליקט.
מלכודת שנייה: !important הופך את סדר השכבות
אם אתם פונים ל-!important בתוך שכבה, סדר העדיפויות בין השכבות מתהפך. עבור הצהרות רגילות, השכבה שהוכרזה מאוחר יותר מנצחת. עבור הצהרות עם !important – מנצחת השכבה שהוכרזה מוקדם יותר.
css
@layer reset, components;@layer reset {.card { padding: 0 !important; }}@layer components {.card { padding: 16px !important; }}/* מנצח ה-reset (0px), כי עם !important העדיפות היא לשכבהשהוכרזה מוקדם יותר – היפוך מדויק של הכללשחל בלי !important */
זו לא גחמה של המפרט – זו הרחבה עקבית של כלל שכבר התקיים עבור ה-origins (גיליונות !important של המשתמש תמיד ניצחו !important של מחבר האתר, בניגוד להצהרות רגילות). המסקנה המעשית: !important בתוך מערכת שכבות מסוגל להפתיע אפילו מישהו שמבין היטב את הסדר הרגיל של @layer – עוד סיבה טובה להתייחס ל-!important כמשהו שפונים אליו במודע ולעיתים רחוקות, לא כדרך ברירת מחדל לנצח בהתמודדות.
מלכודות ושיטות עבודה טובות
- אפס כללים מחוץ לשכבות, אם רוצים שהמערכת תהיה צפויה. כפי שהוצג למעלה, אפילו כלל אחד בלי
@layerמנצח את כל מערכת השכבות עבור הצהרות רגילות – העקביות חשובה יותר מהנוחות של הוספת "רק כלל קטן אחד" תוך כדי תנועה. - הספציפיות עדיין תקפה בתוך שכבה בודדת.
@layerמבטל מלחמות ספציפיות בין שכבות, אבל בתוך שכבה אחת שני כללים עדיין מכריעים קונפליקט בדיוק כמו תמיד – שמות מחלקות הגיוניים והימנעות מקינון עמוק של סלקטורים עדיין משתלמים. - הצהירו על סדר השכבות במפורש, בראש הקובץ, ב-
@layer name1, name2, ...;ריק. זה משחרר אתכם מתלות בסדר הייבוא של הקבצים – סדר העדיפויות נקבע פעם אחת, בבירור, במקום אחד, לא משנה באיזה סדר קבצי ה-CSS השונים בפועל נטענים אחר כך. - תמיכת הדפדפנים כבר לא סיבה לזהירות. Cascade Layers הגיעו למעמד Baseline (זמין באופן נרחב) כבר ב-2022 – Chrome 99, Firefox 97, Safari 15.4 – כך שאפשר לאמץ אותם בלי
@supportsבכל פרויקט חדש.
סיכום
@layer לא מתחרה עם הספציפיות – הוא פועל בשלב אחר לגמרי של אלגוריתם ה-cascade, מוקדם יותר, ובזכות זה מאפשר למחלקת עזר אחת לנצח במודע ובאופן צפוי סלקטור עם שלוש מחלקות מקוננות, בלי להעלות עדיפות ובלי !important. זהו כלי ארכיטקטוני אמיתי להפרדת reset, סגנונות בסיס, רכיבים ומחלקות עזר לשכבות עם סדר חשיבות מוצהר במפורש – אבל שני כללים שלו שוברים אינטואיציה חזק מספיק כדי לזכור אותם בנפרד: סגנון מחוץ לכל שכבה מנצח כל שכבה, ו-!important בתוך שכבות הופך את סדר העדיפויות שלהן. התעלמות מאחת משתי העובדות האלה לא תיגמר בשגיאה בקונסולה – היא תיגמר ב-CSS שפועל אחרת ממה שנראה הגיוני לפי סדר השכבות בקובץ.