CSS 和樣式
本文將進一步探討 Shadow DOM 的其他精彩功能。本課程以「Shadow DOM 101」中討論的概念為基礎。如需簡介,請參閱該文章。
簡介
我們必須承認,未設定樣式的標記並不吸引人。幸好,Web Components 的優秀開發人員預見了這個問題,並為我們提供了CSS Scoping Module 定義了許多選項,可用於設定 Shadow Tree 中的內容樣式。
樣式封裝
Shadow DOM 的核心功能之一是 Shadow 邊界。它有許多優點,其中一項是免費提供樣式封裝。換句話說:
<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,但只有符合 h3 選取器的 ShadowRoot 中的 h3 才會套用紅色樣式。同樣地,預設為限定範圍樣式。
- 這個網頁上定義的其他樣式規則 (目標為 h3) 不會影響我的內容。這是因為選取器不會跨越陰影邊界。
這告訴我們,我們有來自外部世界的樣式封裝。感謝 Shadow DOM!
設定主機元素樣式
:host 可讓您選取並設定託管 Shadow Tree 的元素樣式:
<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) 設定 <x-foo> 的樣式:
:host-context(.different) {
color: red;
}
這樣一來,您就能在元素的 Shadow DOM 中封裝樣式規則,根據其內容設定獨特的樣式。
在單一 Shadow Root 中支援多種主機類型
: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 Tree 中的元素設定樣式。
::shadow 虛擬元素
如果元素至少有一個 Shadow Tree,::shadow 虛擬元素會與 Shadow Root 本身相符。可讓您編寫選取器,為元素 Shadow DOM 內部的節點設定樣式。
舉例來說,如果元素代管 Shadow Root,您可以編寫 #host::shadow span {},為 Shadow Tree 中的所有 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> 個子項。每個面板都有自己的 Shadow Tree,其中包含 h2 標題。如要從主頁面設定這些標題的樣式,可以編寫:
x-tabs::shadow x-panel::shadow h2 {
...
}
/deep/ 組合器
/deep/ 組合器與 ::shadow 類似,但功能更強大。它會完全忽略所有 Shadow DOM 邊界,並跨入任意數量的 Shadow Tree。簡單來說,/deep/ 可讓您深入探究元素內部,並指定任何節點。
/deep/ 組合器在自訂元素中特別實用,因為自訂元素通常會有多個層級的 Shadow DOM。主要例子包括巢狀結構的一堆自訂元素 (每個元素都託管自己的陰影樹狀結構),或使用 <shadow> 建立繼承自另一個元素的元素。
範例 (自訂元素) - 選取樹狀結構中任何位置 <x-tabs> 的所有子系 <x-panel> 元素:
x-tabs /deep/ x-panel {
...
}
範例:為 Shadow Tree 中任何位置、含有 .library-theme 類別的所有元素設定樣式:
body /deep/ .library-theme {
...
}
使用 querySelector()
就像 .shadowRoot 會開啟 Shadow Tree 進行 DOM 遍歷一樣,組合器也會開啟 Shadow Tree 進行選取器遍歷。您可以編寫單一陳述式,而不必編寫巢狀鏈結:
// 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-」前置字元。這樣做會與 Shadow Tree 中的元素建立關聯,並為外部人員提供跨越 Shadow 邊界的指定路徑。
以下範例說明如何建立自訂滑桿小工具,並允許使用者將滑桿圓鈕設為藍色:
<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 屬性,可控制 Shadow 邊界發生的情況。這就像是建立新元件時,從頭開始的方式。
resetStyleInheritance
false:預設值。可沿用的 CSS 屬性會繼續沿用。true:在邊界將可繼承的屬性重設為initial。
以下示範變更 resetStyleInheritance 對 Shadow Tree 的影響:
<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 中,仍是主機元素的子項。插入點只是轉譯功能。
分散式節點會保留主要文件的樣式。也就是說,即使元素在插入點算繪,主頁面的樣式規則仍會繼續套用至元素。同樣地,分散式節點在邏輯上仍位於 Light DOM 中,不會移動。只是在其他地方算繪。不過,當節點分散到 Shadow DOM 時,可以採用 Shadow Tree 中定義的其他樣式。
::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,然後才會套用至 Shadow 內容。這個位置稱為上限。對於
<content>插入點:reset-style-inheritance表示在主機的子項分布在插入點之前,可繼承的 CSS 屬性會設為initial。這個位置稱為下邊界。
結論
身為自訂元素的作者,我們有許多選項可以控制內容的外觀和風格。Shadow DOM 是這個全新世界的基礎。
Shadow DOM 可提供限定範圍的樣式封裝,並可讓我們選擇要讓多少外部世界進入。作者可以定義自訂虛擬元素或加入 CSS 變數預留位置,為第三方提供便利的樣式掛鉤,進一步自訂內容。總而言之,網頁作者可以完全控制內容的呈現方式。