Shadow DOM はスタイル境界です。ページルールは内部に到達せず、コンポーネント ルールは外部に漏洩しません。ただし、継承されたプロパティ(カスタム プロパティを含む)は例外です。以下のチャネルを通じて、意図的にクロスさせます。
:host、:host()、:host-context()
/* The component's own host element, styled from inside its shadow tree. */
:host { display: block; }
/* Only when the host matches a selector (e.g. a reflected state attribute). */
:host([disabled]) { opacity: 0.5; pointer-events: none; }
/* Based on an ancestor in the Light DOM; useful for theming. */
:host-context([theme="dark"]) { background: #111; color: #eee; }
:host-context() は避けてください。これは境界の外に到達して祖先をテストしますが、Firefox と Safari はこの実装を拒否しており、CSSWG はこれ以上の開発を行わない方向で検討しています。これは Chromium 専用の行き止まりであり、将来性のあるオプションではありません。代わりに、継承された CSS カスタム プロパティ(下記)を介してテーマを設定します。コンシューマが祖先にプロパティを設定すると、コンポーネントが境界を越えてそれを読み取ります。これはどこでも機能し、両側を分離したままにします。
継承されたプロパティとカスタム プロパティは境界を越える
継承された CSS プロパティ(color、font-family、line-height、CSS カスタム プロパティ)は、シャドウ境界を通過します。これにより、カスタム プロパティがメインのテーマ設定 API になります。適切なフォールバックを使用してカスタム プロパティを公開します。
:host {
/* Consumers override --card-bg from outside; the fallback keeps it usable. */
background: var(--card-bg, white);
color: var(--card-fg, black);
}
:host で定義されたカスタム プロパティは、ホスト要素を明示的にターゲットとするルールでのみオーバーライドできます。ツリーの上位から継承してオーバーライドできるようにするには、:host で公開プロパティを設定せず、各 var() の代替に依存します。:host で公開プロパティをシードしないでください。
フォールバックを var() ごとに繰り返すのではなく、1 回だけ宣言するには、:host で、公開プロパティを読み取る非公開の「シャドウ」プロパティを定義し、内部で非公開プロパティを参照します。
:host {
--_card-bg: var(--card-bg, white);
--_card-fg: var(--card-fg, black);
}
.card { background: var(--_card-bg); color: var(--_card-fg); }
コンシューマは引き続き外部から --card-bg をオーバーライドします。先頭のアンダースコアは、エイリアスが内部であることを示し、フォールバックは 1 か所にのみ存在します。
::slotted(): 投影された Light DOM のスタイル設定
::slotted(selector) は、スロットに割り当てられた最上位ノードをターゲットにします。子孫には到達できず、Light DOM ルール(コンシューマーが独自のコンテンツを所有する)に負けるため、制御ではなく、軽い修正に使用します。::slotted(...) !important はコンシューマー ルールをオーバーライドできますが、そうするとコンシューマーが所有する権利を持つスタイル設定がオーバーライドされます。これは、見た目のオーバーライドではなく、構造的な修正のために予約してください。
/* Matches a slotted <p>, but NOT a <p> nested inside a slotted <div>. */
::slotted(p) { margin: 0; }
::part() と exportparts: テーマ設定のために内部を公開
内部要素を part="name" でマークすると、コンシューマは内部構造を把握することなく、任意のプロパティでスタイルを設定できます。
<!-- inside the shadow template -->
<button part="submit">Submit</button>
/* consumer stylesheet */
my-card::part(submit) { background: navy; color: white; }
すべての <div> ではなく、いくつかの高レベルのパーツ(ボタン、入力、ラベル)を公開します。広範なパーツ サーフェスは、脆弱な API になります。ネストされたコンポーネントのパーツを独自のコンポーネントを介して上方向に再公開するには、exportparts を使用して転送します。
<inner-widget exportparts="submit: card-submit"></inner-widget>
2 つのチャネルを慎重に選択します。カスタム プロパティは、制約のある、デザイン システムに安全な値のセットに使用します。パーツは、特定の要素の無制限のスタイル設定に使用します。
ラップされたネイティブ要素にスタイルを転送する
カスタム要素がネイティブ コントロールをラップする場合(実際の <button> を囲む <my-button>)、通常、次の 3 つのことが問題になります。
- フォーム コントロールはタイポグラフィを継承しません。
colorとカスタム プロパティは境界を越えますが、<button>、<input>、<select>は UA スタイルシートからfontなどを取得します。これは継承されません。そのため、ラップされたコントロールは、シャドウツリー内でfont: inherit(通常はcolor: inherit)を設定するまで、ページのフォントではなくプラットフォームのフォントでレンダリングされます。これは、最もよくあるミスです。 :hostスタイルは内部要素に到達しません。:hostでbackground/border/paddingを設定すると、ホストボックスのスタイルが設定されます。ホストボックス内のコントロールのスタイルは設定されません。意図的に転送する: 自由なスタイリングのためにコントロールを::partとして公開するか、カスタム プロパティを介して固定値のセットをパイプします。:hostルールは、ラップされた要素にカスケードされません。display: contents+all: inheritは、ショートカットではなく、実際のトレードオフです。display: contentsでホストを折りたたみ、all: inheritで内部コントロールをリセットすることは、現在、コンシューマーがコンポーネントごとのパーツやカスタム プロパティを学習することなく、任意のスタイルをラップされたネイティブ要素に渡す唯一の一般的な方法です。ホスト独自のボックスを削除することは、通常、このパターンのポイントです(組み込みの継承の欠如を補うコンポジション)。2 つの実際の注意事項:all: inheritは UA のデフォルトをワイプするため、特定のプロパティにスコープを設定するか、デフォルトに戻す場合はrevertを使用します。具体的なブロッカーとして、getBoundingClientRect()とgetClientRects()はdisplay: contents要素に対してゼロを返すため、ホストに対するアンカーの配置が壊れます。これらのトレードオフが適切でない場合は、::partまたはカスタム プロパティを使用します。
:state(): コンポーネント独自の状態のスタイルを設定する
カスタム要素は、this.#internals.states.add('busy') と .delete('busy') を介して ElementInternals を通じて内部のブール値の状態を公開できます(ライフサイクルの使用方法については、カスタム要素をご覧ください)。これらの状態は、境界を越えたスタイル設定チャネルになります。::part やカスタム プロパティと同様ですが、構造や値ではなく状態を対象としています。シャドウツリー内では :host(:state(name)) を使用して、コンシューマーのスタイルシートからは tag-name:state(name) を使用して、それらを照合します。
/* Inside the component's shadow tree. */
:host(:state(busy)) button { opacity: 0.6; pointer-events: none; }
/* From the consumer's stylesheet — a read-only public hook. */
favorite-button:state(favorited) { filter: drop-shadow(0 0 4px crimson); }
反映された属性とは異なり、カスタム状態は外部に対して読み取り専用です。コンシューマーはスタイルを設定できますが、設定や偽造はできません。そのため、コンポーネントは唯一のライターのままです。状態がコンポーネントによって所有され、スタイル設定のみが必要な場合は、data-* 属性よりもこちらを使用することをおすすめします。
@layer とコンポーネント内のコンテナクエリ
- カスケード レイヤ(
@layer)はシャドウルート内で機能し、そのスコープに限定されます。シャドウツリー内のレイヤ名は、ページ内の同じ名前のレイヤとは独立しています。これらを使用して、独自のコンポーネント スタイル(@layer base, theme;など)を順序付けします。これにより、カスタム プロパティと::partを介したコンシューマーのオーバーライドが引き続き予測どおりに優先されます。 - コンテナクエリは、レスポンシブ コンポーネントに適したツールです。コンポーネントは、ビューポートではなく、割り当てられたスペースにレスポンシブに対応する必要があります。ホストまたはラッパーをコンテナとして宣言し(カスタム要素はデフォルトで
display: inlineになり、サイズ コンテインメントはinline要素またはcontents要素に適用されないため、:hostにdisplay: blockなどのレイアウト ボックスを指定します)、クエリを実行します。これにより、同じコンポーネントがサイドバーまたは全幅領域に適応します。
:host {
display: block; /* Required: size containment does not apply to default display: inline */
container-type: inline-size;
}
@container (min-width: 30rem) {
.card { grid-template-columns: 1fr 2fr; }
}
代替戦略
::part、カスケード レイヤ、コンテナクエリは広く利用できます。テーマは主に継承されたカスタム プロパティを介して行われます。これらは、これらが機能しない場合でも機能します。前述のように、:host-context() に依存しないでください。これは実質的に Chromium 専用であり、将来を見据えたオプションではありません。