前端如何使用 Shadow DOM ,打造獨立插槽樣式的可重用網頁元件。

再前端專案開發,Web Components(網頁元件) 打造可重用、模組化介面是在工作中不考或缺的能力。而其中比較難的的技術,就是 Shadow DOM

傳統的網頁開發常常會面臨以下問題:

  • CSS 樣式全域污染,修改一處可能影響全站。
  • JavaScript 無法有效隔離邏輯,元件之間容易互相干擾。
  • 多人協作時,元件整合困難、維護成本高。

這些問題, Shadow DOM 就能完美的解決。它透過封裝樣式與行為,讓每個元件都擁有自己的「影子世界」,不受外部影響,進一步提升元件的穩定性與可維護性

Shadow DOM 的核心概念

DOM Tree vs. Shadow Tree

在標準 DOM 中,瀏覽器會建立一棵 DOM Tree(DOM 樹)來表示整體結構。但當你為某個元素建立 Shadow DOM 時,它會產生一個獨立的 Shadow Tree(影子樹),與原始的 DOM Tree 分開存在,對外部不可見。

主頁面 DOM:
<div id="host"></div>

Shadow Tree(掛在 host 上):
#shadow-root
  └── <span>I'm in the shadow DOM</span>

這樣的分離,確保了內容的私密性與可控性。

Light DOM 與 Shadow DOM 的差別

  • Light DOM:就是平常撰寫的 HTML 結構,受全域樣式與 JS 控制。
  • Shadow DOM:掛載在特定元素下的私有結構,只能透過特定方式操作,與主頁面隔離。

Shadow Root:建立封裝的起點

你可以透過 Element.attachShadow() 建立 Shadow Root:

const host = document.querySelector("#host");
const shadow = host.attachShadow({ mode: "open" });

其中 mode 可設定為:

  • open:允許外部透過 .shadowRoot 存取。
  • closed:外部無法存取,封裝更完整。

CSS Scoping:封裝樣式的關鍵

Shadow DOM 會自動隔離內部的 CSS,不會影響外部,也不會被外部影響。這就是所謂的 Scoped CSS。搭配 :host、::part 等選擇器,更能靈活地控制樣式開放程度。

Shadow DOM 的實作

基礎範例:建立一個簡單的 Shadow DOM

範例:

<div id="host"></div>
<button id="upper">將 span 元素轉成大寫</button>
<button id="reload">重新整理</button>

<script>
  const host = document.querySelector("#host");
  const shadow = host.attachShadow({ mode: "open" });

  const span = document.createElement("span");
  span.textContent = "I'm in the shadow DOM";
  shadow.appendChild(span);

  document.querySelector("#upper").onclick = () => {
    shadow.querySelector("span").textContent = span.textContent.toUpperCase();
  };
</script>

這段程式碼,建立了 Shadow DOM 並更新元素。

套用樣式:在 Shadow DOM 中設定 CSS

你可以直接寫入 <style>:

const style = document.createElement("style");
style.textContent = `
  span {
    color: red;
  }
`;
shadow.appendChild(style);

或使用 :host 來設計元件本身的樣式:

:host {
  display: block;
  border: 1px solid #ccc;
}

進階時,也能利用 CSS Variables 與 ::part 打造可自訂的樣式鉤子。

例如:

<!-- my-button 元件 -->
<template id="btn-tpl">
  <style>
    :host {
      display: inline-block;
    }

    button::part(label) {
      padding: 8px 16px;
      background-color: var(--btn-bg, #333);
      color: var(--btn-color, white);
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
  </style>
  <button part="label">Click Me</button>
</template>

<script>
  class MyButton extends HTMLElement {
    constructor() {
      super();
      const shadow = this.attachShadow({ mode: 'open' });
      const tpl = document.getElementById('btn-tpl').content.cloneNode(true);
      shadow.appendChild(tpl);
    }
  }

  customElements.define('my-button', MyButton);
</script>

組件使用者端(外部頁面)

<my-button style="--btn-bg: teal; --btn-color: #fff;"></my-button>

或針對 ::part 選擇器進一步微調樣式(這需要元件內部已使用 part=”…” 標註):

my-button::part(label) {
  font-size: 18px;
  font-weight: bold;
}

插槽 (Slot):自定義內容的關鍵

插槽能讓 Shadow DOM 預留位置供外部插入內容:

<template id="template">
  <style> ::slotted(p) { color: blue; } </style>
  <slot name="title"></slot>
  <slot></slot> <!-- 匿名插槽 -->
</template>

使用方式:

<my-card>
  <h2 slot="title">標題</h2>
  <p>內容段落</p>
</my-card>

你也可以為插槽提供預設的 fallback 內容,確保元件在缺省資料時也能正常顯示。

事件處理:如何讓事件穿透封裝

在 Shadow DOM 中,事件預設不會冒泡到主頁面,除非你顯式指定:

const event = new CustomEvent("custom-click", {
  bubbles: true,
  composed: true,
});
this.dispatchEvent(event);

設定 composed: true 能讓事件穿越 Shadow Tree,傳送給外部父層使用者監聽。

進階應用與實戰注意事項

在 React 中整合 Shadow DOM

雖然 React 並不原生支援 Shadow DOM,但你可以透過 ReactDOM.createPortal 將 React 元件渲染進 Shadow Root:

const root = host.attachShadow({ mode: "open" });
ReactDOM.createPortal(<MyComponent />, root);

這種方法適合建立封裝式 Widget、微前端子應用等,但需注意:

  • React 的事件系統可能無法穿透 Shadow DOM
  • 需手動處理樣式與事件綁定

Accessibility 與 SEO 注意事項

封裝不代表放棄無障礙設計。請務必加入:

  • aria-* 屬性
  • 正確的標籤結構與 role
  • 顯示文字與可聚焦元素(tabindex)

Shadow DOM 與 SEO:搜尋引擎爬蟲看得到嗎?

傳統 Shadow DOM(用 JavaScript 動態掛載)

不利 SEO!搜尋引擎看不到內容。

大多數搜尋引擎(例如 Googlebot)雖然支援 JavaScript,但對於「透過 JavaScript 建立的 Shadow DOM」仍然無法完整解析其內部內容,原因如下:

  • Shadow DOM 是獨立於主 DOM 結構的封裝區塊,預設不可見。
  • 搜尋引擎在渲染時可能無法進入 .shadowRoot 取得內容。
  • 使用 mode: closed 的 Shadow DOM 更是完全不可存取。

因此,像這樣的內容:

<div id="host"></div>

<script>
  const shadow = document.querySelector('#host').attachShadow({ mode: 'open' });
  const span = document.createElement('span');
  span.textContent = '這段文字在 Shadow DOM 裡';
  shadow.appendChild(span);
</script>

解法與 SEO 友善策略

使用「Declarative Shadow DOM」(宣告式 Shadow DOM)

這是 HTML 標準的新功能,讓你在 HTML 中直接宣告 Shadow DOM 結構,不靠 JavaScript 動態建立。

<div shadowroot="open">
  <span>這段內容可以被索引!</span>
</div>

好處:

  • 搜尋引擎可讀取內容(特別是 Googlebot)
  • 無需等待 JS 解析與渲染
  • 更利於 SSR(Server Side Rendering)

缺點:

  • 尚未被所有瀏覽器完整支援(截至 2025 年,Chrome/Edge/Opera 支援較好)

使用 slot 傳入可索引內容

即便你使用了 Shadow DOM,也可以在外部 HTML 中用 <slot> 傳入可被搜尋引擎讀取的內容:

<my-component>
  <h1 slot="title">SEO 可見標題</h1>
</my-component>

在 Shadow DOM 中:

<slot name="title"></slot>

相容性

主流瀏覽器皆支援 Shadow DOM(Chrome、Firefox、Safari、Edge)。如需支援 IE,可考慮使用 webcomponents.js

贊助商連結

發佈留言