Shadow DOM 201

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 {
    ...
}

使用自訂的擬造元素

WebKitFirefox 都定義了虛擬元素,用於設定原生瀏覽器元素的內部部分樣式。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 變數預留位置,作者就能提供第三方方便的樣式鉤子,進一步自訂內容。總而言之,網頁作者可完全控制內容的呈現方式。