CSS 和樣式
本文將進一步說明 Shadow DOM 的更多驚人功能。這項功能是以「Shadow DOM 101」一文中討論的概念為基礎。如要瞭解相關資訊,請參閱這篇文章。
簡介
我們面對現實吧。沒有樣式的標記並沒有什麼吸引力,幸運的是,Web 元件背後的優秀團隊預見了這個問題,並沒有讓我們空等。CSS 範圍設定模組定義了許多選項,可用於設定陰影樹狀結構中的內容樣式。
樣式封裝
Shadow DOM 的核心功能之一是陰影邊界。它具有許多不錯的屬性,其中最棒的一個是免費提供樣式封裝。換句話說:
<div><h3>Light DOM</h3></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = `
<style>
h3 {
color: red;
}
</style>
<h3>Shadow DOM</h3>
`;
</script>
這個示範有兩個有趣的觀察結果:
- 這個頁面上還有其他 h3,但只有 ShadowRoot 中的 h3 符合 h3 選取器,因此會以紅色顯示樣式。再次強調,預設會使用區隔樣式。
- 本頁定義的其他樣式規則 (針對 h3) 不會流入我的內容。這是因為選取器不會跨越陰影邊界。
故事的寓意是什麼?我們有來自外部的樣式封裝。感謝 Shadow DOM!
為主機元素設定樣式
:host
可讓您選取並設定代管陰影樹狀結構的元素:
<button class="red">My Button</button>
<script>
var button = document.querySelector('button');
var root = button.createShadowRoot();
root.innerHTML = `
<style>
:host {
text-transform: uppercase;
}
</style>
<content></content>
`;
</script>
其中一個陷阱是,父項頁面中的規則比元素中定義的 :host
規則更具特異性,但比主機元素中定義的 style
屬性更不具特異性。這樣一來,使用者就能從外部覆寫您的樣式。:host
也只能在 ShadowRoot 的內容中運作,因此無法在 Shadow DOM 外使用。
:host(<selector>)
的函式形式可讓您指定主機元素 (如果符合 <selector>
)。
範例:只有在元素本身含有類別 .different
(例如 <x-foo class="different"></x-foo>
) 時才比對:
:host(.different) {
...
}
回應使用者狀態
:host
的常見用途是建立自訂元素時,並想回應不同的使用者狀態 (:hover、:focus、:active 等)。
<style>
:host {
opacity: 0.4;
transition: opacity 420ms ease-in-out;
}
:host(:hover) {
opacity: 1;
}
:host(:active) {
position: relative;
top: 3px;
left: 3px;
}
</style>
為元素設定主題
如果主機元素或其任何祖系元素與 <selector>
相符,:host-context(<selector>)
擬類別就會與主機元素相符。
:host-context()
的常見用途是根據元素周圍環境為元素設定主題。舉例來說,許多人會將類別套用至 <html>
或 <body>
來進行主題設定:
<body class="different">
<x-foo></x-foo>
</body>
當 <x-foo>
是具有 .different
類別的元素的子項時,您可以使用 :host-context(.different)
為其設定樣式:
:host-context(.different) {
color: red;
}
這樣一來,您就能在元素的 Shadow DOM 中封裝樣式規則,根據元素的內容為其設定獨特的樣式。
在單一陰影根目錄中支援多種主機類型
:host
的另一個用途是,如果您要建立主題設定程式庫,並且想在同一個 Shadow DOM 中支援多種主機元素的樣式設定。
:host(x-foo) {
/* Applies if the host is a <x-foo> element.*/
}
:host(x-foo:host) {
/* Same as above. Applies if the host is a <x-foo> element. */
}
:host(div) {
/* Applies if the host element is a <div>. */
}
從外部為 Shadow DOM 內部設定樣式
::shadow
擬元素和 /deep/
組合器就像是 CSS 權威的 Vorpal 劍。可穿過 Shadow DOM 的邊界,為陰影樹狀結構中的元素設定樣式。
::shadow 擬造元素
如果元素至少有一個陰影樹狀結構,::shadow
擬元素會與陰影根本身相符。您可以使用這個類別編寫選取器,為元素陰影 DOM 內部的節點設定樣式。
舉例來說,如果元素代管陰影根,您可以編寫 #host::shadow span {}
,為陰影樹狀結構中的所有時距設定樣式。
<style>
#host::shadow span {
color: red;
}
</style>
<div id="host">
<span>Light DOM</span>
</div>
<script>
var host = document.querySelector('div');
var root = host.createShadowRoot();
root.innerHTML = `
<span>Shadow DOM</span>
<content></content>
`;
</script>
範例 (自訂元素) - <x-tabs>
的 Shadow DOM 中有 <x-panel>
個子項。每個面板都會代管自己的陰影樹狀結構,其中包含 h2
標題。如要為主頁面中的標題設定樣式,可以編寫以下程式碼:
x-tabs::shadow x-panel::shadow h2 {
...
}
/deep/ 組合函式
/deep/
組合函式與 ::shadow
類似,但功能更強大。它會完全忽略所有陰影邊界,並跨越任意數量的陰影樹狀結構。簡單來說,/deep/
可讓您深入瞭解元素的內部結構,並指定任何節點。
/deep/
組合運算子在自訂元素中特別實用,因為 Shadow DOM 通常會有多個層級。最常見的例子是巢狀化一組自訂元素 (每個元素都會代管自己的陰影樹狀結構),或是使用 <shadow>
建立從其他元素繼承的元素。
範例 (自訂元素):選取樹狀結構中任何位置的所有 <x-panel>
元素,這些元素是 <x-tabs>
的子項:
x-tabs /deep/ x-panel {
...
}
範例:在陰影樹狀結構的任何位置,為所有具有 .library-theme
類別的元素設定樣式:
body /deep/ .library-theme {
...
}
使用 querySelector()
就像 .shadowRoot
會為 DOM 檢索作業開啟陰影樹狀結構一樣,組合器也會為選取器檢索作業開啟陰影樹狀結構。您可以編寫單一陳述式,而非編寫瘋狂的巢狀鏈結:
// No fun.
document.querySelector('x-tabs').shadowRoot
.querySelector('x-panel').shadowRoot
.querySelector('#foo');
// Fun.
document.querySelector('x-tabs::shadow x-panel::shadow #foo');
設定原生元素的樣式
原生 HTML 控制項的樣式難以調整。許多人乾脆放棄,自行捲動。不過,只要使用 ::shadow
和 /deep/
,網頁平台中任何使用 Shadow DOM 的元素都可以套用樣式。<input>
類型和 <video>
就是很好的例子:
video /deep/ input[type="range"] {
background: hotpink;
}
建立樣式開場
自訂功能很不錯,在某些情況下,您可能會想在 Shadow 的樣式盾牌中開洞,並建立鉤子,讓其他人設定樣式。
使用 ::shadow 和 /deep/
/deep/
的效能非常強大。這可讓元件作者將個別元素指定為可設定樣式的元素,或將大量元素指定為可設定主題的元素。
範例:為所有具有 .library-theme
類別的元素套用樣式,並忽略所有陰影樹狀結構:
body /deep/ .library-theme {
...
}
使用自訂的擬造元素
WebKit 和 Firefox 都定義了虛擬元素,用於設定原生瀏覽器元素的內部部分樣式。input[type=range]
就是一個很好的例子。您可以指定 ::-webkit-slider-thumb
,為滑桿拇指 <span style="color:blue">blue</span>
設定樣式:
input[type=range].custom::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: blue;
width: 10px;
height: 40px;
}
就像瀏覽器在某些內部元件中提供樣式掛鉤一樣,Shadow DOM 內容的作者可以將特定元素指定為可由外部提供樣式的元素。這項作業是透過自訂疑似元素完成。
您可以使用 pseudo
屬性,將元素指定為自訂擬造元素。其值或名稱必須加上「x-」前置字串。這樣做可在陰影樹狀結構中建立與該元素的關聯,並為外部提供跨越陰影邊界的指定車道。
以下範例說明如何建立自訂滑桿小工具,並允許使用者將滑桿拇指設為藍色:
<style>
#host::x-slider-thumb {
background-color: blue;
}
</style>
<div id="host"></div>
<script>
var root = document.querySelector('#host').createShadowRoot();
root.innerHTML = `
<div>
<div pseudo="x-slider-thumb"></div>' +
</div>
`;
</script>
使用 CSS 變數
CSS 變數是建立主題設定鉤子的好方法。基本上,就是為其他使用者建立「樣式預留位置」。
假設自訂元素作者在 Shadow DOM 中標示變數預留位置。一個用於設定內部按鈕的字型樣式,另一個用於設定顏色:
button {
color: var(--button-text-color, pink); /* default color will be pink */
font-family: var(--button-font);
}
接著,元素的嵌入者會依喜好定義這些值。或許是為了配合自己頁面超酷的 Comic Sans 主題:
#host {
--button-text-color: green;
--button-font: "Comic Sans MS", "Comic Sans", cursive;
}
由於 CSS 變數的繼承方式,一切都很順利,而且運作得非常完美!整個圖片如下所示:
<style>
#host {
--button-text-color: green;
--button-font: "Comic Sans MS", "Comic Sans", cursive;
}
</style>
<div id="host">Host node</div>
<script>
var root = document.querySelector('#host').createShadowRoot();
root.innerHTML = `
<style>
button {
color: var(--button-text-color, pink);
font-family: var(--button-font);
}
</style>
<content></content>
`;
</script>
重設樣式
可繼承的樣式 (例如字型、顏色和行高) 會繼續影響 Shadow DOM 中的元素。不過,為了提供最大的彈性,Shadow DOM 會提供 resetStyleInheritance
屬性,用於控制陰影邊界發生的情況。您可以將其視為建立新元件時的全新起點。
resetStyleInheritance
false
- 預設。可繼承的 CSS 屬性會繼續繼承。true
:在邊界重設可繼承的屬性為initial
。
以下是示範,說明變更 resetStyleInheritance
會如何影響陰影樹狀結構:
<div>
<h3>Light DOM</h3>
</div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.resetStyleInheritance = <span id="code-resetStyleInheritance">false</span>;
root.innerHTML = `
<style>
h3 {
color: red;
}
</style>
<h3>Shadow DOM</h3>
<content select="h3"></content>
`;
</script>
<div class="demoarea" style="width:225px;">
<div id="style-ex-inheritance"><h3 class="border">Light DOM</div>
</div>
<div id="inherit-buttons">
<button id="demo-resetStyleInheritance">resetStyleInheritance=false</button>
</div>
<script>
var container = document.querySelector('#style-ex-inheritance');
var root = container.createShadowRoot();
//root.resetStyleInheritance = false;
root.innerHTML = '<style>h3{ color: red; }</style><h3>Shadow DOM<content select="h3"></content>';
document.querySelector('#demo-resetStyleInheritance').addEventListener('click', function(e) {
root.resetStyleInheritance = !root.resetStyleInheritance;
e.target.textContent = 'resetStyleInheritance=' + root.resetStyleInheritance;
document.querySelector('#code-resetStyleInheritance').textContent = root.resetStyleInheritance;
});
</script>

瞭解 .resetStyleInheritance
有點複雜,主要是因為它只會影響可繼承的 CSS 屬性。內容如下:當您在網頁和 ShadowRoot 之間的邊界尋找要繼承的屬性時,請勿從主機繼承值,而是使用 initial
值 (依 CSS 規格)。
如果不確定 CSS 會繼承哪些屬性,請參閱這份實用清單,或是在「元素」面板中切換「顯示繼承項目」核取方塊。
設定分散式節點的樣式
分散式節點是在插入點 (<content>
元素) 處算繪的元素。<content>
元素可讓您從 Light DOM 選取節點,並在 Shadow DOM 中預先定義的位置轉譯這些節點。從邏輯上來說,它們並未位於 Shadow DOM 中,而是仍為主機元素的子項。插入點只是用於轉譯。
分散式節點會保留主要文件中的樣式。也就是說,即使元素在插入點算繪,主要頁面的樣式規則仍會繼續套用至元素。同樣地,分散式節點仍在邏輯上位於輕量 DOM 中,且不會移動。只是在其他地方顯示。不過,當節點分散到 Shadow DOM 時,可以採用陰影樹中定義的其他樣式。
::content 擬似元素
分散節點是主機元素的子項,因此我們如何從「Shadow DOM」內指定目標?答案是 CSS ::content
虛擬元素。這是一種指定通過插入點的 Light DOM 節點的方式。例如:
::content > h3
會為任何經過插入點的 h3
標記設定樣式。
以下面這段程式碼為例:
<div>
<h3>Light DOM</h3>
<section>
<div>I'm not underlined</div>
<p>I'm underlined in Shadow DOM!</p>
</section>
</div>
<script>
var div = document.querySelector('div');
var root = div.createShadowRoot();
root.innerHTML = `
<style>
h3 { color: red; }
content[select="h3"]::content > h3 {
color: green;
}
::content section p {
text-decoration: underline;
}
</style>
<h3>Shadow DOM</h3>
<content select="h3"></content>
<content select="section"></content>
`;
</script>
在插入點重設樣式
建立 ShadowRoot 時,您可以選擇重設沿用樣式。<content>
和 <shadow>
插入點也提供這個選項。使用這些元素時,請在 JS 中設定 .resetStyleInheritance
,或是在元素本身使用布林 reset-style-inheritance
屬性。
針對 ShadowRoot 或
<shadow>
插入點:reset-style-inheritance
表示可繼承的 CSS 屬性會在主機上設為initial
,然後才會影響陰影內容。這個位置稱為上限。針對
<content>
插入點:reset-style-inheritance
表示在主機的子項在插入點分發前,可繼承的 CSS 屬性會設為initial
。這個位置稱為下限。
結論
身為自訂元素的作者,我們有許多選項可用來控制內容的外觀和風格。Shadow DOM 是這個新世界的基礎。
Shadow DOM 可讓我們將樣式封裝在特定範圍內,並提供一種方法,讓我們選擇要讓外部世界介入多少 (或完全不介入)。定義自訂的擬造元素或納入 CSS 變數預留位置,作者就能提供第三方方便的樣式鉤子,進一步自訂內容。總而言之,網頁作者可以完全控制內容的呈現方式。