再前端專案開發,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。
