Shadow DOM と CSS による Web Components のスタイリング
ページに Web Component を配置したとき、なぜグローバル CSS がその内部で効かなくなるのか不思議に思った経験があるなら、それは shadow boundary に遭遇したということです。これはバグではなく、まさに意図された挙動です。Shadow DOM はカスタム要素に独自のスコープ化された DOM ツリーを与えるため、スタイルはデフォルトでは内外に漏れません。しかし、それはスタイリングが不可能だという意味ではなく、スタイリングが意図的なものになるということです。
本記事では、Shadow DOM スタイリングの主要なメカニズムを取り上げます: 内部スタイル、:host、::slotted()、CSS カスタムプロパティ、::part()、そして adoptedStyleSheets です。
要点
- Shadow DOM は設計上スタイルをスコープ化するため、グローバル CSS はコンポーネント内部に届かず、内部スタイルも外に漏れません。
- Shadow スタイルはデフォルトとして機能し、カスケード上ではページの作成スタイルの下に位置します。これはブラウザのユーザーエージェントスタイルシートに近い扱いです。
- コンポーネント要素自体をスタイルするには
:hostと:host()を使い、投影された light DOM の子要素(直接の子孫のみ)をスタイルするには::slotted()を使います。 - CSS カスタムプロパティと
::part()は、意図的に設計された公開スタイリング API を形成します: テーマ用トークンには変数を、構造的スタイリングには parts を使います。 adoptedStyleSheetsを使えば、複数のコンポーネントで単一のパース済みCSSStyleSheetを共有でき、大規模利用時のパフォーマンスが向上します。
なぜ Shadow DOM は CSS の挙動を変えるのか
要素に対して attachShadow() を呼び出すと、その要素(shadow host)にアタッチされた別個の DOM ツリーが作成されます。ページのスタイルシート内のセレクタはそのツリー内部に到達できず、シャドウツリー内部のセレクタもツリー外部に到達できません。
理解しておくべき重要なニュアンスが一つあります: Shadow DOM のスタイルは、CSS カスケード上では作成者のスタイルの下に位置します。つまり、* { color: red } のようなグローバルルールは、たとえそれがソース順序で先に現れていたとしても、シャドウ内部の :host { color: green } ルールを上書きします。Shadow スタイルは、ブラウザのユーザーエージェントスタイルシートが <button> や <input> にデフォルトを提供するのと同様に、コンポーネントのデフォルトスタイルだと考えてください。
:host を使った Shadow Host のスタイリング
シャドウツリー内部からは、:host がそのシャドウがアタッチされた要素を選択します:
:host {
display: block;
font-family: sans-serif;
}
また、:host(selector) を使うことで、host 上の属性やクラスに基づいて条件付きでスタイルを適用することもできます:
:host([disabled]) {
opacity: 0.5;
pointer-events: none;
}
host 要素は light DOM に存在するため、ドキュメントレベルのスタイルが :host ルールを上書きします。コンポーネントのデフォルトを優先させたい場合は、シャドウ内部で !important を使うことになります。これは !important の使用が本当に適切な数少ない場面の一つです。
::slotted() によるスロットコンテンツのスタイリング
スロットを使うと、light DOM のコンテンツをシャドウツリーに投影できます。::slotted() 擬似要素を使えば、シャドウ内部からそれら投影された要素をスタイリングできます:
::slotted(p) {
margin: 0;
color: #333;
}
重要な制限: ::slotted() はスロットに直接割り当てられた要素のみにマッチし、その子孫要素にはマッチしません。::slotted(p span) のようなセレクタは機能せず、::slotted() は複合セレクタ(子孫結合子は不可)しか受け付けません。より深い階層のスタイリングには、CSS の継承を利用するか、light DOM 側のスタイルに任せましょう。
CSS カスタムプロパティは Shadow Boundary を越える
CSS 変数(カスタムプロパティ)は、shadow boundary を自由に貫通します。これにより、Web Components のテーマ設定において最も柔軟なツールとなります:
/* シャドウツリー内 */
:host {
background: var(--card-bg, white);
color: var(--card-color, black);
}
/* ページのスタイルシート内 */
my-card {
--card-bg: #1a1a2e;
--card-color: #eee;
}
コンポーネントがフックを定義し、利用者が値を設定します。外部設定なしでもコンポーネントが機能するよう、常にフォールバック値を含めましょう。
Discover how at OpenReplay.com.
CSS Shadow Parts (::part()) によるスタイルフックの公開
CSS Shadow Parts は、Web Component の内部要素を外部からスタイリングするための現代的な答えです。コンポーネント内部で、part 属性を使って要素にマークを付けます:
<button part="trigger">Open</button>
コンポーネントの外側からは、利用者が ::part() を使ってその part を直接ターゲットできます:
my-dialog::part(trigger) {
background: royalblue;
border-radius: 4px;
}
これは意図的に設計されたスタイリング API です。コンポーネント作成者が何を公開するかを決定します。CSS 変数よりも構造的スタイリング(レイアウト、ボーダー、背景)に強力で、それでいて内部実装の詳細は非公開のまま保てます。
adoptedStyleSheets による効率的なスタイル共有
adoptedStyleSheets を使えば、CSSStyleSheet オブジェクトをシャドウルートに直接アタッチできます。これはモダンブラウザ全般で幅広くサポートされており、多くのインスタンスでパース済みのスタイルシートを共有する必要があるコンポーネントライブラリに最適です:
const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);
class MyComponent extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets = [sheet];
}
}
customElements.define('my-component', MyComponent);
ブラウザは、何個のコンポーネントが使用しようと、スタイルシートを一度だけパースします。これは大規模利用時に意味のあるパフォーマンス上の利点となります。adoptedStyleSheets 配列を変更することでスタイルシートを追加・削除でき、共有された CSSStyleSheet への変更はそれが適用されているすべての場所に反映されます。
適切なアプローチの選択
| 目的 | ツール |
|---|---|
| コンポーネントの host 要素をスタイリングする | :host / :host() |
| 投影された light DOM コンテンツをスタイリングする | ::slotted() |
| 利用者にテーマトークンを公開する | CSS カスタムプロパティ |
| 利用者に構造的スタイリングを公開する | ::part() |
| 多数のコンポーネントでスタイルを共有する | adoptedStyleSheets |
まとめ
Shadow DOM スタイリングは CSS を締め出すためのものではなく、カプセル化を明示的にするためのものです。デフォルトには内部スタイルと :host を、公開スタイリング API には CSS 変数と ::part() を、大規模時のパフォーマンスが重要な場面では adoptedStyleSheets を使いましょう。このメンタルモデルを身につければ、Web Components のスタイリングは難しくありません。
FAQ
そのコンポーネントが Shadow DOM を使用しており、メインドキュメントから隔離されたスコープ化された DOM ツリーを作成しているためです。ページのスタイルシートはシャドウツリー内部の要素を選択できません。内部をスタイリングするには、コンポーネント作成者が CSS カスタムプロパティや、part 属性を介した CSS Shadow Parts などのフックを公開する必要があり、利用者はそれらを外部から ::part() でターゲットします。
色、間隔、フォントなど、継承を通じて自然に流れるデザイントークンにはカスタムプロパティを使ってください。一方、ボタンやヘッダーのボーダー、背景、レイアウトをオーバーライドするなど、特定の内部要素に対して利用者が構造的な制御を必要とする場合には ::part() を使います。Parts はより細かいアクセスを提供し、変数はよりシンプルで広範なスコープを保ちます。
::slotted() 擬似要素は、スロットに直接割り当てられたトップレベルのノードのみにマッチし、その子孫にはマッチしません。また複合セレクタしか受け付けないため、子孫結合子は無効です。スロットされた要素の子要素をスタイリングするには、スロットされた要素からの CSS 継承を利用するか、利用者側の light DOM スタイルシートにそれらの子孫の処理を直接任せてください。
はい。すべてのモダンなエバーグリーンブラウザでサポートされており、再パースなしに多数のシャドウルート間でスタイルを共有する推奨される方法です。1 つのパース済み CSSStyleSheet を複数のシャドウルートにアタッチでき、メモリを削減し起動時間を改善します。共有されたスタイルシートは、複数のコンポーネント間で効率的に更新・再利用することもできます。
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.