Light DOM

Lightning Web Components では現在、すべてのコンポーネントが Shadow DOM に強制されます。この場合、コンポーネントの内部マークアップがカプセル化され、プログラムコードにアクセスできなくなります。Light DOM を使用すると、コンポーネントが Shadow DOM の外部になるため、Shadow DOM の制限を回避できます。このアプローチにより、サードパーティインテグレーションやグローバルスタイル設定が容易になります。

Light DOM の詳細を確認する前に、各種の DOM 構造が DOM でどのようにレンダリングされるかを説明します。まず、シャドウツリーのマークアップを考えてみましょう。ネイティブ Shadow DOM は、#shadow-root タグ内のコンポーネントを表示します。ただし、Lightning Experience と Experience Cloud では、ネイティブ Shadow の動作を模倣する合成 Shadow が代用されます。

Light DOM では、コンポーネントのコンテンツは、シャドウツリーではなく、ホスト要素に添付されます。ドキュメントホスト内の他のコンテンツと同様にアクセスすることができ、Shadow DOM によってバインドされていないコンテンツと同様の動作をします。

詳細は Google Web Fundamentals の「Shadow DOM v1」を参照してください。

Light DOM は、Shadow DOM と比べていくつかの利点があります。

  • CSS のテーマおよびブランド設定: Light DOM はグローバルなスタイル設定をサポートしており、コンポーネントや子コンポーネントにカスタムブランド設定を簡単に適用できます。
  • サードパーティのツールとテストの使用: Light DOM を使用した場合、サードパーティツールで DOM をトラバースできます。これは querySelectorquerySelectorAll などのブラウザー標準のクエリ API に対応し、シャドウルートをトラバースすることなく使用できます。たとえば、Light DOM では、LWR サイトの標準コンポーネントでイベントを追跡できます。
  • アクセシビリティ: Light DOM では ID の範囲が設定されないため、2 つの独立したコンポーネントで他方の ID を参照できます。たとえば、<label for="my-input"> は、要素が別々のコンポーネントにあっても <input type="text" id="my-input"> を参照できます。

Light DOM を使用すると、コンポーネントを DOM スクレイピングで隠すことができないため、機密データを使用する場合は、代わりに Shadow DOM を使用することをお勧めします。つまり、Light DOM ではシャドウツリーへの不正アクセスを防止する Shadow DOM カプセル化の利点を活用できなくなります。DOM は他のコンポーネントやサードパーティツールによるトラバースに対して制限がないため、Light DOM コンポーネントのセキュリティ保護についてはお客様が独自に行っていただく必要があります。

アプリケーションで Light DOM と Shadow DOM の両方を使用する場合は、次のベストプラクティスを考慮してください。

  • Light DOM の子コンポーネントを Shadow DOM の親コンポーネントにネストすることも、その逆のネストも可能です。
  • 深くネストされた Light DOM コンポーネントは、最上位レベルの単一の Shadow DOM でカプセル化することをお勧めします。次に、シャドウルートの配下にあるすべてのコンポーネント間でスタイルを共有できます。
  • ドキュメントレベルで要素の照会やスタイルの挿入を行うときは注意が必要です。ドキュメントとコンポーネントの間にシャドウツリーが存在する可能性があります。
  • Shadow DOM のスタイルは、CSS カスタムプロパティ::part によって上書きできます。ただし、拡張ポイントの公開はコンポーネントの所有者が担当するため、ダウンストリームのコンシューマーが任意の要素にスタイルを設定することはできません。
  • Light DOM を特定の名前空間に制限することはサポートされていません。
  • Light DOM でレンダリングされるコンポーネントの配布はサポートされていません。管理パッケージ内のコンポーネント参照は c 名前空間を使用するため、名前空間の衝突が発生します。
  • 基本コンポーネントは常に Shadow DOM でレンダリングされます。
  • Aura コンポーネントは Light DOM を使用できません。ただし、Light DOM を使用する LWC コンポーネントを Aura コンポーネントに含めることはできます。
  • slot 要素は DOM でレンダリングされないため、スロットのライフサイクルフックが呼び出されることはありません。
  • for:each イテレーター内でスロットを使用することはサポートされていません。次に例を示します。

Lightning LockerLightning Web セキュリティでは、最上位の Light DOM コンポーネントはサポートされません。Light DOM コンポーネントは必ず、Shadow DOM コンポーネント内のどこかの場所でネストする必要があります。

エクスペリエンスビルダーサイトなどで Locker を無効にすると、Locker のセキュリティの利点を得られなくなります。緩和された CSP 環境のエクスペリエンスビルダーサイトへの影響を理解してください。「CSP および Lightning Locker の設計に関する考慮事項」を参照してください。

Aura ベースのエクスペリエンスビルダーサイトでは、this.querySelector() や他の Web API を使用する DOM アクセスは Lightning Locker によってブロックされます。Aura ベースの Experience Cloud サイトで Light DOM を使用する場合、1 つ以上の LWC Shadow DOM コンポーネントが Light DOM コンポーネントの上位に存在していることを確認します。Shadow DOM コンポーネントを使用する場合、コンポーネントの内部マークアップがカプセル化され、Shadow DOM の制限が適用されます。

Winter ’23 以降、Lightning Web セキュリティ (LWS) が組織で有効になっている場合は、Aura サイトに含まれている Lightning Web コンポーネントが Lightning Locker ではなく LWS によって保護されます。サイトで Lightning Locker を無効にする場合は、Lightning Web セキュリティも無効にしてください。

コンポーネントの作成には、強力なカプセル化を利用できる Shadow DOM が推奨されます。コンポーネントの内部を隠して、コンシューマーには公開 API のみを利用できるようにします。

Shadow DOM は次の状況には適していません。

  • Web アプリケーションの外観を完全に制御する必要がある。この場合は高度にカスタマイズ可能な UI を作成してください。
  • サードパーティライブラリを使用している。多くの一般的なライブラリは、Shadow DOM に対応していません。

このような場合には Light DOM の方が適していますが、コンシューマーがパブリック API と同様にコンポーネントの内部にもアクセスできることに注意が必要です。そのようなアクセスを許可すると、コンシューマーのコードに影響することなく変更を実装することが困難になります。

ここでは、どちらか一方を使う場合の利点と不利な点について説明します。

Shadow DOMLight DOM
セキュリティ強力なコンポーネントのカプセル化により、不正アクセスからコンポーネントが保護される脆弱なカプセル化のため、不正アクセスに対してコンポーネントが無防備になる
可搬性公開 API でアクセスが制御されるため可搬性が高いコンポーネントの作成者やコンシューマーによる変更の影響を受けやすい
スタイル設定スタイルを上書きするための CSS カスタムプロパティが必要スタイルの上書きが容易
サードパーティライブラリおよびツールのインテグレーションDOM トラバースやイベント対象の変更を必要とするサードパーティライブラリまたはツールの互換性に制限があるサードパーティライブラリおよびツールとの単純なインテグレーション

Google アナリティクスやその他の計測ライブラリなどのサードパーティライブラリを使用する場合、Shadow DOM コンポーネントで適切な API を公開していれば、Light DOM を使用する必要はありません。たとえば、ボタンのクリック操作を調査する必要があるとします。

Light DOM の場合、button 要素にクリックイベントのリスナーを接続できます。コンポーネントを Shadow DOM でレンダリングした場合、button 要素はコンポーネントの外部からアクセスできません。Shadow DOM では、<button>my-button コンポーネントの内部実装の詳細になります。この場合、このコンポーネントを調査する正しい方法は、my-button 自体にクリックハンドラーを追加して調査することです。

内部イベントを公開すると、コンポーネントのカプセル化が脆弱になるため、必要最低限のもののみを公開してください。上記の例が常に可能とは限らないため、まず可能な選択肢を探してから、ユースケースに最適なものを選択することをお勧めします。

Light DOM を有効にするには、まずコンポーネントクラスに renderMode 静的プロパティを設定します。

次に、lwc:render-mode ルートテンプレートディレクティブを使用します。これは、Light DOM を使用するコンポーネントに必要です。

インスタンス化後に renderMode 静的プロパティの値を変更しても、コンポーネントのレンダリングが Light DOM と Shadow DOM いずれになるかには影響しません。

コンポーネントを Shadow DOM から Light DOM に移行するには、コードを変更する必要があります。シャドウツリーは、CSS、イベント、および DOM の操作方法に影響します。Light DOM を使用する場合は、次のセクションに示す相違点について考慮してください。

アプリには、Shadow DOM または Light DOM のいずれかを使用するコンポーネントを含めることができます。このサンプルテンプレートでは、my-app で Shadow DOM が使用されており、my-header コンポーネントと my-footer コンポーネントが含まれています。前者では Light DOM、後者では Shadow DOM が使用されています。

Light DOM には Shadow DOM コンポーネントを含めることができます。同様に、Shadow DOM コンポーネントには Light DOM コンポーネントを含めることができます。

深くネストされたコンポーネントがある場合は、最上位レベルに単一の Shadow DOM を使用し、Light DOM コンポーネントをネストするように検討してください。この構造により、1 つのシャドウルート内のすべての子コンポーネントでスタイルを自由に共有できます。

合成 Shadow やネイティブ Shadow と異なり、Light DOM は ID の範囲を個々のコンポーネントに設定することはありません。代わりに、コンポーネントで別のコンポーネントの ID を参照できるようになります。この利点により、2 つの要素を同じシャドウルートに配置することで、ID と ARIA 属性を使用してリンクできます。

2 つの兄弟コンポーネントを使用する次の例を考えてみます。

c-label コンポーネントには、id 属性を持つ <label> 要素が含まれています。

c-input コンポーネントには、c-label コンポーネントの <label> 要素を参照する <input> 要素が含まれています。

Shadow DOM を使用した場合、親コンポーネントで定義された CSS スタイルは、子コンポーネントに適用されません。一方、Light DOM を使用すると、ルートドキュメントから DOM ノードを対象にして、スタイルを設定できます。

次のネイティブ Shadow コンポーネントのスタイルは、子コンポーネントの Light DOM にカスケードされます。このケースでは、Light DOM コンポーネントはネイティブ Shadow コンポーネント内にあり、最も近いネイティブ Shadow ルートレベルでマウントされます。これは、その Shadow ルート全体の内側でローカルに範囲設定され、そのルート内のすべての Light DOM コンポーネントに影響します。

同様に、Light DOM でレンダリングされる子コンポーネントのスタイルは、ネイティブ Shadow DOM を使用している場合はシャドウ境界までその親コンポーネントに適用されます。

合成 Shadow DOM の場合、Shadow DOM のスタイルは Light DOM の子コンポーネントにカスケードされません。

合成 Shadow DOM では、スタイルはグローバルドキュメントレベルで実装されますが、属性を使用してスタイルの範囲が設定されます。これが合成 Shadow DOM の現在の制限です。

LWC では、自動的にスタイルの範囲が設定されません。スタイルがコンポーネントからカスケードすることを防ぐために、*.scoped.css ファイルによって範囲設定されたスタイルを使用することをお勧めします。**「Light DOM での範囲設定されたスタイルの使用」**セクションを参照してください。

Lightning Web コンポーネントの継承されたスタイルを上書きするには、コンポーネントスタイルシートで SLDS スタイル設定フックを作成します。スタイル設定フックは、カスタムスタイルのプレースホルダーとして機能します。スタイル設定フックをサポートするコンポーネントブループリントのリストについては、「Blueprint Overview (ブループリントの概要)」を参照してください。

Light DOM コンポーネントがレンダリングされる順序は、スタイルシートがルートノードに挿入される順序に影響し、CSS ルールの特異性に直接影響します。

Shadow DOM では、コンポーネントが所有する要素にのみアクセスできます。

一方、Light DOM コンポーネントではノードを取得できます。これは、サードパーティのインテグレーションやテストに役立ちます。たとえば、document.querySelector('p') を使用してアプリのパラグラフを照会できます。

Shadow DOM を使用した場合、LightningElement.prototype.template がコンポーネントに関連付けられたシャドウルートを返します。Light DOM を使用するコンポーネントでは template 要素を使用できません。そのため、Light DOM を使用した場合、LightningElement.prototype.template は、null を返します。

Shadow DOM コンポーネントを Light DOM に移行する場合は、this.template.querySelectorthis.querySelector に置き換えてください。次の例では、Light DOM コンポーネントを使用するために、一般的な DOM API のリストを使用しています。

Light DOM コンポーネントでは、this.querySelectorAll() で、他の Light DOM コンポーネントによってレンダリングされた要素を返すことができます。

要素の id 属性は実行時に保護されるため、合成 Shadow DOM のように操作されることはありません。実行時に要素の id を照合できるため、CSS または JavaScript で id セレクターを使用できます。

Light DOM と Shadow DOM のコンポーネントを操作するときは this.refs を使用してください。this.querySelectorthis.querySelectorAll とは異なり、this.refs は、コンポーネントで定義されている要素にアクセスし、Light DOM と Shadow DOM の両方で同じように動作します。

Shadow DOM の場合、イベントが上に伝達されてシャドウ境界を横断したときには、いくつかのプロパティ値がリスナーの範囲に応じて変更されます。Light DOM の場合は、イベントの対象が変更されません。Light DOM コンポーネントの複数のレイヤー内でネストされているボタンをクリックすると、ドキュメントレベルで click イベントにアクセスできます。また、event.target が、内部のコンポーネントではなく、イベントをトリガーしたボタンを返します。

たとえば、Light DOM を使用する c-light-child コンポーネントがあり、Light DOM を使用する c-light-container コンテナコンポーネントにネストされているとします。最上位レベルの c-app コンポーネントは Shadow DOM を使用します。

c-light-child でカスタム buttonclick イベントをディスパッチすると、ハンドラーが次の要素を返します。

c-light-child ホストハンドラー

  • event.currentTarget: c-light-child
  • event.target: c-light-child

c-light-container ホストハンドラー

  • event.currentTarget: c-light-container
  • event.target: c-light-child

対照的に、c-light-container で Shadow DOM を使用する場合、イベントはシャドウルートをエスケープしません。

シャドウルートがないため、Light DOM で composedfalse の場合でも、イベントが上に伝達されます。

Shadow DOM の外部のスロットはブラウザーでサポートされていないため、Light DOM でエミュレートされます。Light DOM のスロットは、合成シャドウスロットと同様に動作します。LWC は、スロットが Light DOM を実行しているかどうかを実行時に判断します。

my-component という名前付きスロットと名前なしスロットを持つコンポーネントがあるとします。

この場合は、次のようなコンポーネントを使用します。

これらの <slot> 要素は、DOM にレンダリングされません。コンテンツは、DOM のホスト要素に直接追加されます。

スロットコンテンツまたはフォールバックコンテンツは、実行時に親要素でフラット化されます。<slot> 要素自体は描画されないので、<slot> 要素に属性やイベントリスナーを追加すると、コンパイラーエラーが発生します。

さらに、c-shadow-slot-container Shadow DOM コンポーネントと c-light-slot-container Light DOM コンポーネントを含む次の Light DOM コンポーネント c-light-slot-consumer について考えてみましょう。

c-app にスタイルを含めると、_スロット内_のすべての要素 (Shadow DOM と Light DOM コンポーネントの両方) がスタイルを取得します。ただし、_スロットのない Shadow DOM コンポーネント_はスタイルを取得しません。

スロットを使用するこれらの構成モデルを使用することを検討してください。Light DOM のコンポーネントでは、コンテンツやその他のコンポーネントでスロットを作成できます。スロットは、Light DOM コンポーネントと Shadow DOM コンポーネントの両方をサポートします。

コンテンツをスロットでレンダリングする方法は次のとおりです。

スロット要素は DOM でレンダリングされないため、slotchange イベントと ::slotted CSS 疑似セレクターはサポートされません。

Light DOM では、スロットに割り当てられていないスロット化された要素がレンダリングされないため、ライフサイクルフックがコールされることはありません。

Light DOM スロットでは、slot 属性を使用してスロットコンテンツを別の名前付きスロットに転送できません。Light DOM <slot> 要素では、デフォルトスロットへのスロットコンテンツの転送のみがサポートされています。

名前付きスロットが設定された <span> 要素があるとします。

c-outer コンポーネントには、転送されるコンテンツのslot プロパティが含まれる <slot> 要素があります。

この c-inner コンポーネントでは、<c-inner> の「forwardedSlot」という名前のスロットに「Named slot content」という文字列が表示されるはずですが、表示されません。

スロットコンテンツを別の名前付きスロットに転送するには、別の要素の最も外側の <slot> 要素をラップして slot 属性をその要素に割り当てます。

デフォルトスロットでも同様です。

範囲設定されたスロットを使用すると、子コンポーネントのデータにアクセスし、それを親コンポーネント内のスロットコンテンツに表示できます。子コンポーネントのデータを範囲設定されたスロットにバインドすると、親コンポーネントは、子コンポーネントのデータをスロットコンテンツで参照できます。このデータは、子コンポーネントの Light DOM でスロット化されます。

次の例では、子コンポーネント <c-child> がその item データを範囲設定されたスロット <slot> にバインドします。親コンポーネント <c-parent> では、範囲設定されたスロットフラグメントが、<c-child> にスロット化されるマークアップの {item.id}{item.name} を参照します。

範囲設定されたスロットフラグメントは、親コンポーネントのテンプレートにあるため、親コンポーネントがスロットコンテンツを所有します。そのため、親コンポーネントが範囲設定されたスタイルシートを参照する場合は、それらのスタイルも範囲設定されたスロットのコンテンツに適用されます。

親コンポーネントは、範囲設定されたスロットのコンテンツを部分的に表示するため、そのコンテンツを <template></template> タグで囲む必要があります。この例の場合、範囲設定されたスロットのコンテンツは <span>{item.id} - {item.name}</span> です。親コンポーネントは、この部分的なフラグメントを作成します。

親コンポーネントは各 item も表示し、子コンポーネントはループロジックを制御します。<c-child> は、<c-parent> から渡された同じテンプレートフラグメントを使用して、スロットを必要な数だけ作成します。

範囲設定されたスロットを使用する場合、子コンポーネントは Light DOM を使用する必要があります。Shadow DOM の範囲設定されたスロットはサポートされていません。親は Light DOM または Shadow DOM コンポーネントにできます。

最終的な HTML は次のようになります。

範囲設定されたスロットをコンポーネントに挿入するには、ディレクティブ lwc:slot-bind および lwc:slot-data を追加します。詳細は、「スロットのディレクティブ」「ネストされたテンプレートのディレクティブ」を参照してください。

子コンポーネントには、複数の範囲設定されたスロットを含めることができますが、デフォルトの範囲設定されたスロットは 1 つしか含めることができません。

さまざまな範囲設定されたスロットを同じデータソースにバインドできます。次の例では、デフォルトの範囲設定されたスロットと 2 つの名前付きの範囲設定されたスロットが slotdata からのコンテンツを表示します。

範囲設定されたスロットは 1 つのデータソースにのみバインドできます。たとえば、名前付きの範囲設定されたスロット namedslotA を 2 つの異なるデータセットである slot1dataslot2data にバインドすると、コンパイルエラーが発生します。

デフォルトの範囲設定されたスロットを複数の異なるデータセットにバインドしようとすると、コンパイラーは同じエラーをスローします。

コンポーネントに設定できるデフォルトスロットは 1 つのみであるため、標準のデフォルトスロットとデフォルトの範囲設定されたスロットを同じコンポーネントに配置することはできません。次のコードではエラーが発生します。

子コンポーネント内で、名前付きの範囲設定されたスロットと標準の名前付きスロットが同じ名前を共有することはできません。次のコードではエラーが発生します。

親コンポーネントの範囲設定されたスロットを子コンポーネントからのデータにバインドするときは、両方のコンポーネントに同じ種別のスロットが含まれている必要があります。たとえば、子コンポーネントにバインド済みの範囲設定されたスロットが親コンポーネントに含まれている場合は、その子コンポーネントにも範囲設定されたスロットが必要です。それ以外の場合、スロットコンテンツは表示されません。デバッグモードを有効にすると、エラーが発生した場合に開発者コンソールにもログが記録されます。

範囲設定されたスロットは、別の範囲設定されたスロット内にネストできます。

範囲設定されたスロットは、コンポーネントのバインドと範囲のバインドを参照できます。

Light DOM では、範囲設定されたスタイルを使用して、コンポーネントの要素にのみ CSS を適用できます。この動作は、Shadow DOM を使用したスタイルのカプセル化に似ています。

範囲設定されたスタイルをコンポーネントに追加するには、コンポーネントフォルダーに *.scoped.css ファイルを作成します。

上記の例では、Shadow DOM および Light DOM コンポーネントについて、CSS ファイルをいずれかまたは両方に含めることができ、また、いずれにも含めないこともできます。

Aura ベースのコンテナでは、Light DOM コンポーネントは範囲設定されたスタイルのみを読み込むことができます。たとえば、Aura ベースのエクスペリエンスビルダーサイトでは、カスタムコンポーネントに *.css ファイルではなく *.scoped.css ファイルを含める必要があります。

ここで、範囲設定されたスタイルを持つ Light DOM について確認します。

範囲設定されたスタイルの結果は次のようになります。

*.css ファイルが *.scoped.css ファイルと一緒に使用されている場合、*.css スタイルシートは *.scoped.css スタイルシートの前に挿入されます。範囲設定されたスタイルの CSS セレクターは、最後に宣言される範囲設定されていない CSS セレクターよりも優先されます。

範囲設定されたスタイルシートと範囲設定されていないスタイルシートがテンプレートで使用されている場合は、両方のスタイルシートが適用されます。挿入の順序は、セレクターを複製したときにブラウザーによって適用されるスタイルに影響します。「文書ツリー内の近接性の無視」を参照してください。

この場合、c-light-cmp は範囲設定されたスタイルを使用しますが、範囲設定されていないスタイルシートのスタイルがコンポーネントから流出する可能性があります。

前の例では、c-app のパラグラフは lightCmp.css からスタイルを継承しています。lightCmp.css からのスタイルを上書きするには、範囲設定されたスタイルシート app.scoped.css を含めます。

デバッグが難しくなるため、!important ルールを使用することはお勧めしません。範囲設定されたスタイルシートと範囲設定されていないスタイルシートの両方のスタイル宣言で !important を使用すると、範囲設定されたスタイルの特異性が高まり、そのスタイルがコンポーネントに適用されます。

*.scoped.css を使用すると、CSS セレクターはコンポーネント HTML ファイル内のすべての要素に範囲が設定されます。

次の CSS では、すべてのセレクターがテンプレートの要素にのみ一致します。

ルート要素 c-light-cmp は、Light DOM コンポーネント内でも、:host を使用して対象に指定できます。

CSS の範囲設定では、カスタム CSS クラスを使用して、スタイルがコンポーネントから漏洩するのを防ぐことができます。

Light DOM スタイルはデフォルトで範囲が指定されていないため、:host 疑似セレクターは最も近いシャドウルートホスト要素 (存在する場合) を参照します。Light DOM で範囲設定されたスタイルでは、:host セレクターは、範囲設定された DOM 領域のルートである Light DOM コンポーネントのルート要素を参照します。

Light DOM コンポーネントに範囲設定されていないおよび範囲設定されたスタイルシートがあるとします。

コンポーネントは次のレンダリングを実行します。

この例では :host が範囲設定されたスタイルに変換されています。

:host-context() セレクターはサポートされていません。

Light DOM の範囲設定されたスタイルは、LWC の合成シャドウ範囲設定されたスタイルといくつかの違いがあります。

  • Light DOM で範囲設定されたスタイルは範囲指定にクラスを使用しますが、合成 Shadow DOM は HTML 属性を使用します。
  • Light DOM で範囲設定されたスタイルは、範囲設定されたスタイルシート内で @import をサポートしていません。
  • Light DOM の範囲設定されたスタイルは、lwc:dom="manual" 内のテンプレートに手動で挿入されたコンテンツには適用されません。たとえば、Element.appendChild または Element.innerHTML を使用して挿入されたコンテンツなどです。

関連トピック