設定網頁元件樣式

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 自訂屬性) 會穿過 Shadow 邊界。因此,自訂屬性是主要的佈景主題 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> 會從 UA 樣式表取得 font 和相關屬性,而 UA 樣式表「不會」繼承。因此,除非您在 Shadow Tree 中設定 font: inherit (通常是 color: inherit),否則包裝的控制項會以平台字型 (而非頁面字型) 算繪。這是最常見的失誤。
  • :host 樣式不會套用至內部元素。在 :host 上設定 background/border/padding 會設定主機方塊的樣式,而非其中的控制項。刻意轉送:將控制項公開為 ::part,以便進行開放式樣式設定,或透過自訂屬性傳送一組固定值,:host 規則不會串連至包裝元素。
  • display: contents + all: inherit 是真正的權衡,而不是捷徑。目前,使用 display: contents 摺疊主體並使用 all: inherit 重設內部控制項,是將任意樣式傳遞至包裝的原生元素的唯一通用方法,而使用者無需瞭解每個元件部分或自訂屬性,且移除主體本身的方塊通常是該模式的重點 (組合補償缺少的內建繼承)。有兩個實際注意事項:all: inherit 會抹除 UA 預設值,因此請限定特定屬性的範圍,或在需要還原預設值時使用 revert;以及具體阻礙因素:getBoundingClientRect() 和 getClientRects() 會針對 display: contents 元素傳回零,因此相對於主機的錨定位置會中斷。如果這些取捨不符合需求,請使用 ::part 或自訂屬性。

:state():設定元件本身狀態的樣式

自訂元素可透過 this.#internals.states.add('busy') 和 .delete('busy'),經由 ElementInternals 公開內部布林值狀態 (如要瞭解生命週期用法,請參閱自訂元素),這些狀態會成為跨越邊界的樣式管道,就像 ::part 和自訂屬性一樣,但適用於狀態,而非結構或值。在 Shadow Tree 中使用 :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) 會在 Shadow Root 內運作,且範圍限定在 Shadow Root 內;Shadow Tree 中的層名稱與頁面中同名的層無關。請使用這些項目來排序自己的元件樣式 (例如 @layer base, theme;),這樣一來,消費者透過自訂屬性和 ::part 覆寫時,仍可如預期勝出。
  • 容器查詢是回應式元件的合適工具:元件應回應其提供的空間,而非可視區域。將主機或包裝函式宣告為容器 (為 :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,並非前瞻性的選項。