This article was translated from Japanese by AI and may contain inaccuracies. For the most accurate content, please refer to the original Japanese version.
ハロウィンのコウモリのイラスト

HTML だけで Shadow DOM を構築するための宣言型 Shadow DOM

宣言型 Shadow DOM は `<template>` 要素を使用して Shadow DOM を構築する方法です。宣言型 Shadow DOM を使用することで、従来の JavaScript を使用した Shadow DOM の構築方法と比較して、サーバーサイドレンダリング(SSR)に対応しているため、パフォーマンスの向上や SEO 対策に期待されます。

Shadow DOM は Web Components を構成する 3 つの技術の 1 つです。Shadow DOM はコンポーネントのカプセル化を実現します。Shadow DOM で定義されたスタイルは Shadow DOM の外部に影響を与えず、また外部のスタイルの影響を受けません。

Shadow DOM は再利用可能なコンポーネントを構築するために重要な技術ですが、従来は JavaScript を使用しなければ Shadow DOM を構築できないという問題がありました。Shadow DOM を構築するためには、ホストとなる DOM 要素で attachShadow メソッドを使用して Shadow DOM を構築し、innerHTML プロパティもしくは appendChild メソッドを使用して Shadow DOM に HTML を追加する必要があります。

const host = document.querySelector("#host");
// mode が open の場合、Shadow DOM は外部の JavaScript からアクセス可能
const shadowRoot = host.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
  <style>
    p {
      color: red;
    }
  </style>
  <p>Hello, Shadow DOM!</p>
`;

JavaScript を用いたアプローチの欠点は、サーバー側でレンダリングできないことです。多くのアプリケーションではコンテンツが描画されるまでのパフォーマンスを向上させたり、SEO 対策のためにサーバーサイドレンダリング(SSR)を行っています。サーバーで生成された HTML には Shadow DOM が含まれていないため、クライアント側で Shadow DOM を構築する必要があり、パフォーマンスに影響を与えます。また、ページの読み込み後にコンテンツが挿入されるため、レイアウトシフトが発生する可能性もあります。

宣言型 Shadow DOM(Declarative Shadow DOM)は、上記の問題を解決するために提案され、現在では Baseline 2024 に組み込まれているため、すべてのモダンブラウザで利用可能です。

宣言型 Shadow DOM を作成する

宣言型 Shadow DOM は、<template> 要素を使用して Shadow DOM を構築します。<template> 要素に shadowrootmode 属性の値に open または closed を指定することで HTML パーサーにより Shadow DOM が構築されます。shadowrootmode 属性に渡す値は attachShadow メソッドの mode パラメータと同じです。open の場合、外部の JavaScript から Shadow DOM にアクセス可能になります。

<hello-shadow-dom>
  <template shadowrootmode="open">
    <style>
      p {
        color: red;
      }
    </style>
    <p>Hello, Shadow DOM!</p>
  </template>
</hello-shadow-dom>

カスタム要素と組み合わせる

宣言型で作成された Shadow DOM に対して後からカスタム要素を組み合わせることができます。用途としては、サーバー側で生成された Shadow DOM にクライアント側でカスタム要素を登録することにより、クリックイベントなどの動的な振る舞いを追加することが挙げられます。

まずはじめに <template> 要素を使用して Shadow DOM を構築します。

<shadow-button>
  <template shadowrootmode="open">
    <button>Click me!</button>
  </template>
</shadow-button>

次に JavaScript を使用してカスタム要素を登録します。カスタム要素を作成するためには、HTMLElement クラスを継承したクラスを作成し、customElements.define メソッドを使用して登録します。

HTMLElement を継承したクラス内では shadowRoot プロパティにアクセスできます。このプロパティにはカスタム要素にすでにアタッチされている Shadow DOM が格納されています。connectedCallback メソッド内で this.shadowRoot が存在するかどうかを確認し、存在する場合は Shadow DOM を JavaScript から作成する必要はありません。そうでない場合には対応する Shadow ルートが存在しないため、attachShadow メソッドを使用して Shadow DOM を作成します。

class ShadowButton extends HTMLElement {
  constructor() {
    super();
  }
 
  connectedCallback() {
    if (this.shadowRoot) {
      const button = this.shadowRoot.querySelector("button");
      button.addEventListener("click", () => {
        alert("Hello, Shadow DOM!");
      });
    } else {
      const shadowRoot = this.attachShadow({ mode: "open" });
      shadowRoot.innerHTML = `
      <button>Click me!</button>
    `;
      shadowRoot.querySelector("button").addEventListener("click", () => {
        alert("Hello, Shadow DOM!");
      });
    }
  }
}

customElements.define メソッドを使用してカスタム要素を登録する際の第 1 引数は宣言型 Shadow DOM で使用したカスタム要素の名前と一致している必要があります。

customElements.define("shadow-button", ShadowButton);

これでボタンをクリックするとアラートが表示されるカスタム要素が作成されました。

JavaScript から宣言型 Shadow DOM を作成する

JavaScript から宣言型 Shadow DOM を作成する場合、従来の Shadow DOM の作成方法と同様に attachShadow メソッドを使用します。しかし、innerHTMLinsertAdjacentHTML メソッドはセキュリティ上の理由から宣言型 Shadow ルートが適用された HTML を解析できません。

const host = document.querySelector("#host");
const shadowRoot = host.attachShadow({ mode: "open" });
shadowRoot.innerHTML = `
  <hello-shadow-dom>
    <template shadowrootmode="open">
      <style>
        p {
          color: red;
        }
      </style>
      <p>Hello, Shadow DOM!</p>
    </template>
`;

上記のコードを実行するとコンソールに以下の Warning が表示され、画面には何もレンダリングされません。

Found declarative shadowrootmode attribute on a template, but declarative Shadow DOM is not being parsed. Use setHTMLUnsafe() or parseHTMLUnsafe() instead.

Shadow ルートが適用された HTML を解析するためには、setHTMLUnsafe メソッドまたは parseHTMLUnsafe メソッドを使用します。

Note

それぞれのメソッド名のプレフィックスについている Unsafe はスクリプトをサニタイズせずに実行するため XSS のリスクがあることを示しています。将来デフォルトでサニタイズされて実行される setHTMLparseHTML メソッドが仕様に追加される予定です。https://github.com/WICG/sanitizer-api/blob/main/explainer.md

const host = document.querySelector("#host");
const shadowRoot = host.attachShadow({ mode: "open" });
shadowRoot.setHTMLUnsafe(`
  <hello-shadow-dom>
    <template shadowrootmode="open">
      <style>
        p {
          color: red;
        }
      </style>
      <p>Hello, Shadow DOM!</p>
    </template>
`);

まとめ

  • 従来の Shadow DOM の作成方法は JavaScript を使用する必要があり、サーバーサイドレンダリング(SSR)に対応していないという問題があった
  • 宣言型 Shadow DOM は <template> 要素で shadowrootmode 属性の値に open または closed を指定することで HTML パーサーにより Shadow DOM を構築することができる
  • shadowrootmode 属性の値が open の場合は外部の JavaScript から Shadow DOM にアクセス可能になる
  • HTMLElement クラスを継承したクラス内では shadowRoot プロパティにアクセスでき、カスタム要素にすでにアタッチされている Shadow DOM が格納されている
  • JavaScript から宣言型 Shadow DOM を作成する場合、セキュリティ上の理由から innerHTMLinsertAdjacentHTML メソッドは使用できない。代わりに setHTMLUnsafe または parseHTMLUnsafe メソッドを使用する

参考

Comprehension check

Answer the following questions to deepen your understanding of the article.

宣言型 Shadow DOM はどの要素を使用して Shadow DOM を構築するか?

  • <shadow-dom mode="open">

    Try again

  • <shadow-root mode='open'>

    Try again

  • <template shadowrootmode="open">

    Correct!

  • <shadow-dom shadowrootmode="open">

    Try again