Shadow DOM 201

CSS 和樣式

本文將探討 Shadow DOM 的眾多強大功能。此類別以 Shadow DOM 101 中討論的概念為基礎。如需相關說明,請參閱該文章。

簡介

面對現實,沒有樣式的標記是沒有可愛的。對我們來說,網頁元件幕後傑出的傑出成員深知這件事,讓我們感到很開心。CSS 範圍模組定義了可在陰影樹狀結構中為內容設定樣式的多個選項。

樣式封裝

影子 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 選取器的 h3s,因此只有 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 的常見用途是,您在建立「自訂元素」,並想要回應不同的使用者狀態 (:懸停、: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 語錄,它們允許穿過陰影 DOM 邊界,為陰影樹狀結構中的元素設定樣式。

::shadow 虛擬元素

如果元素至少有一個陰影樹狀結構,::shadow 虛擬元素會與陰影根本身相符。可讓您編寫選取器,設定節點內部符合元素陰影區的樣式。

舉例來說,如果元素代管陰影根,您可以編寫 #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-tabs> 子系 <x-panel> 元素:

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 和 /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 之間的邊界上,不要沿用主機的值,而是根據 CSS 規格使用 initial 值。

如果不確定 CSS 會沿用哪些屬性,請查看這份方便清單,或切換「元素」面板中的「顯示沿用的項目」核取方塊。

設定分散式節點樣式

分散式節點是在「插入點」 (<content> 元素) 算繪的元素,<content> 元素可讓您從 Light DOM 中選取節點,並算繪到 Shadow DOM 的預先定義位置。這些元素在 Shadow 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,然後再將主機的子項發布至插入點。這個位置稱為「下限」

結論

身為自訂元素的作者,我們有許多選項可以控制內容的外觀和風格。陰影 DOM 是賦予這個嶄新世界的基礎。

Shadow DOM 提供範圍限定樣式的封裝方式,也可讓你隨心所欲挑選出外界。作者可以定義自訂虛擬元素或包含 CSS 變數預留位置,提供第三方便利的樣式掛鉤,進一步自訂內容。總而言之,網頁作者可以完全掌控內容的呈現方式。