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,但只有一個 h3 與 h3 選取器相符,因此變成具有樣式的紅色,則是 ShadowRoot 的樣式。同樣地,限定範圍的預設樣式。
- 本頁定義的其他樣式規則 (針對 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>
設定元素主題
如果 :host-context(<selector>)
虛擬類別或其任何祖系與 <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 中封裝樣式規則,根據元素的內容為其設定獨特的樣式。
在單一陰影根目錄中支援多種主機類型
: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 變數預留位置,作者就能提供第三方方便的樣式鉤子,進一步自訂內容。總而言之,網頁作者可完全控制內容的呈現方式。