Shadow DOM הוא גבול סגנון: כללי הדף לא מגיעים פנימה וכללי הרכיבים לא דולפים החוצה, למעט מאפיינים שעוברים בירושה (כולל מאפיינים מותאמים אישית). אפשר להעביר אותו בכוונה דרך הערוצים הבאים.
:host, :host(), :host-context()
/* The component's own host element, styled from inside its shadow tree. */
:host { display: block; }
/* Only when the host matches a selector (e.g. a reflected state attribute). */
:host([disabled]) { opacity: 0.5; pointer-events: none; }
/* Based on an ancestor in the Light DOM; useful for theming. */
:host-context([theme="dark"]) { background: #111; color: #eee; }
הימנעו מ:host-context(). הוא מגיע מחוץ לגבול כדי לבדוק את האבות הקדמונים, אבל Firefox ו-Safari סירבו להפיץ אותו ו-CSSWG נוטה לא לפתח אותו יותר – התייחסו אליו כמבוי סתום שרלוונטי רק ל-Chromium, ולא כאפשרות לעתיד. במקום זאת, אפשר להגדיר עיצוב באמצעות מאפיין CSS מותאם אישית שעובר בירושה (בהמשך): הצרכן מגדיר את המאפיין ברכיב אב, והרכיב קורא אותו מעבר לגבול. השיטה הזו פועלת בכל מקום ושומרת על הפרדה בין שני הצדדים.
מאפיינים שעוברים בירושה ומאפיינים מותאמים אישית חוצים את הגבול
מאפייני CSS שעוברים בירושה (color, font-family, line-height ומאפייני CSS מותאמים אישית) כן עוברים דרך גבול הצל. כך המאפיינים המותאמים אישית הופכים ל-API הראשי של העיצוב: חושפים אותם עם חלופות הגיוניות.
:host {
/* Consumers override --card-bg from outside; the fallback keeps it usable. */
background: var(--card-bg, white);
color: var(--card-fg, black);
}
אפשר לשנות מאפיין מותאם אישית שמוגדר ב-:host רק באמצעות כללים שמטרגטים את הרכיב המארח באופן ספציפי. כדי לאפשר לו לרשת ולהיות ניתן לשינוי מכל מקום בעץ, צריך להשאיר את המאפיין הציבורי לא מוגדר ב-:host ולהסתמך על ברירת המחדל בכל var(); אין להגדיר את המאפיין הציבורי ב-:host.
כדי להגדיר את ברירת המחדל פעם אחת במקום לחזור עליה בכל var(), מגדירים מאפיין "צל" פרטי ב-:host שקורא את המאפיין הציבורי, ומפנים למאפיין הפרטי באופן פנימי:
:host {
--_card-bg: var(--card-bg, white);
--_card-fg: var(--card-fg, black);
}
.card { background: var(--_card-bg); color: var(--_card-fg); }
הצרכנים עדיין משנים את --card-bg מבחוץ. הקו התחתון המוביל מסמן שהכינוי הוא פנימי, והחלופה נמצאת בדיוק במקום אחד.
::slotted(): עיצוב של Light DOM מוטל
::slotted(selector) מטרגט את הצמתים ברמה העליונה שמוקצים למיקום מודעה. הוא לא יכול להגיע לצאצאים שלהם, והוא מפסיד לכל כלל של Light DOM (הצרכן הוא הבעלים של התוכן שלו), לכן כדאי להשתמש בו לתיקונים קלים, ולא לשליטה. אפשר להשתמש ב-::slotted(...) !important כדי לעקוף את הכללים של הצרכן, אבל זה מבטל את הסגנון שהצרכן זכאי להגדיר בעצמו. לכן, כדאי להשתמש בזה רק לתיקונים מבניים, ולא לשינויים אסתטיים.
/* Matches a slotted <p>, but NOT a <p> nested inside a slotted <div>. */
::slotted(p) { margin: 0; }
::part() ו-exportparts: חשיפת רכיבים פנימיים לצורך עיצוב
כדי לאפשר לצרכנים לעצב רכיב פנימי באמצעות כל מאפיין, בלי ללמוד את המבנה הפנימי, מסמנים אותו ב-part="name".
<!-- inside the shadow template -->
<button part="submit">Submit</button>
/* consumer stylesheet */
my-card::part(submit) { background: navy; color: white; }
חשפו כמה חלקים ברמה גבוהה (כפתור, קלט, תווית), לא כל <div>; משטחי חלקים רחבים הופכים ל-API שביר. כדי לחשוף מחדש את החלקים של רכיב מוטמע כלפי מעלה דרך הרכיב שלכם, העבירו אותם באמצעות exportparts:
<inner-widget exportparts="submit: card-submit"></inner-widget>
בחרו בין שני הערוצים באופן מכוון: מאפיינים מותאמים אישית לקבוצה מוגבלת של ערכים שמתאימה למערכת העיצוב, או חלקים לעיצוב ללא הגבלה של רכיב ספציפי.
העברת סגנונות לרכיב מקורי עטוף
כשאלמנט מותאם אישית כולל פקד מקורי (<my-button> סביב <button> אמיתי), יש שלושה דברים שבדרך כלל מבלבלים אנשים:
- פקדי טפסים לא מקבלים בירושה את הטיפוגרפיה.
colorומאפיינים מותאמים אישית חוצים את הגבול, אבל<button>,<input>ו-<select>לוקחים אתfontורכיבים דומים מגיליון הסגנונות של UA, שלא עובר בירושה. לכן, הפקד המעטפת מוצג בגופן של הפלטפורמה, ולא בגופן של הדף, עד שמגדירים אתfont: inherit(ובדרך כלל גם אתcolor: inherit) בתוך עץ הצל. זו הטעות הכי נפוצה. - הסגנונות של
:hostלא מגיעים לרכיב הפנימי. ההגדרהbackground/border/paddingב-:hostמעצבת את תיבת המארח, ולא את הפקד שבתוכה. העברה מכוונת: חשיפת הפקד כ-::partלעיצוב פתוח, או העברת קבוצה קבועה של ערכים דרך מאפיינים מותאמים אישית – כלל:hostלא עובר בירושה לאלמנט העטוף. display: contents+all: inheritהיא פשרה אמיתית, לא קיצור דרך. כיווץ המארח באמצעותdisplay: contentsואיפוס הפקד הפנימי באמצעותall: inheritהם כרגע הדרך הגנרית היחידה להעביר סגנונות שרירותיים לאלמנט נייטיב עטוף בלי שהצרכנים ילמדו חלקים לכל רכיב או מאפיינים מותאמים אישית – והסרת התיבה של המארח עצמו היא בדרך כלל המטרה של התבנית (קומפוזיציה שמפצה על ירושה מובנית חסרה). שתי אזהרות אמיתיות:all: inheritמוחק את ברירות המחדל של UA, אז צריך להגדיר היקף לנכסים ספציפיים או להשתמש ב-revertכשרוצים להחזיר את ברירות המחדל; והחוסם הקונקרטי –getBoundingClientRect()ו-getClientRects()מחזירים אפס לרכיביdisplay: contents, כך שהמיקום של העוגן מול המארח לא תקין. אם הפשרות האלה לא מתאימות, אפשר להשתמש ב-::partאו במאפיינים מותאמים אישית.
:state(): עיצוב המצבים של הרכיב עצמו
רכיב מותאם אישית יכול לחשוף מצבים בוליאניים פנימיים דרך ElementInternals באמצעות this.#internals.states.add('busy') ו-.delete('busy') (אפשר לעיין ברכיבים מותאמים אישית כדי לראות איך משתמשים במחזור החיים), והמצבים האלה הופכים לערוץ עיצוב מעבר לגבול – כמו ::part ומאפיינים מותאמים אישית, אבל למצב ולא למבנה או לערכים. אפשר להתאים אותם בתוך עץ הצל באמצעות :host(:state(name)), ומגיליון סגנונות של צרכן באמצעות tag-name:state(name):
/* Inside the component's shadow tree. */
:host(:state(busy)) button { opacity: 0.6; pointer-events: none; }
/* From the consumer's stylesheet — a read-only public hook. */
favorite-button:state(favorited) { filter: drop-shadow(0 0 4px crimson); }
בניגוד למאפיין משתקף, מצב מותאם אישית הוא לקריאה בלבד מבחוץ: הצרכנים יכולים לעצב אותו אבל לא להגדיר או לזייף אותו, כך שהרכיב נשאר הכותב היחיד. עדיף להשתמש בו על פני מאפיין data-* בכל פעם שהמצב נמצא בבעלות הרכיב וצריך רק לעצב אותו.
@layer ושאילתות קונטיינר בתוך רכיבים
- שכבות מדורגות (
@layer) פועלות בתוך שורש צל ומוגבלות אליו. שם השכבה בעץ הצל לא תלוי בשכבה עם אותו שם בדף. אפשר להשתמש בהן כדי לסדר את סגנונות הרכיבים (למשל@layer base, theme;) כך ששינויים של הצרכנים באמצעות מאפיינים מותאמים אישית ו-::partעדיין ינצחו באופן צפוי. - שאילתות מאגר הן הכלי המתאים לרכיבים רספונסיביים: רכיב צריך להגיב למרחב שניתן לו, ולא לאזור התצוגה. מגדירים את המארח או את ה-wrapper כקונטיינר (נותנים ל-
:hostתיבת פריסה כמוdisplay: block, כי רכיבים בהתאמה אישית מוגדרים כברירת מחדל ל-display: inlineוהכלה של גודל לא חלה על רכיביinlineאוcontents) ושולחים אליו שאילתה, כדי שאותו רכיב יתאים את עצמו בסרגל צדדי או באזור ברוחב מלא.
:host {
display: block; /* Required: size containment does not apply to default display: inline */
container-type: inline-size;
}
@container (min-width: 30rem) {
.card { grid-template-columns: 1fr 2fr; }
}
אסטרטגיות חלופיות
::part, שכבות מדורגות ושאילתות קונטיינר זמינות באופן נרחב. העיצוב מתבצע בעיקר באמצעות מאפיינים מותאמים אישית שעוברים בירושה, שפועלים גם במקומות שבהם הם לא פועלים. כפי שצוין למעלה, אין להסתמך על :host-context() – היא מיועדת למעשה רק ל-Chromium ולא מהווה אפשרות לעתיד.