Shadow DOM は、Webコンポーネント(Web Components) と呼ばれる技術群のひとつです。見栄え(スタイル)がカプセル化されたテンプレートを用いて、データを表示していくことが可能です。サポート状況は下記で確認できます。
最も簡単なサンプルを下記に示します。Shadow DOM では、Shadow Host 要素の子要素を Shadow Root 要素の子要素で置き換えることにより実現します。
<div id="my-host"> <p>Good morning!</p> </div> <script> const shadowHost = document.getElementById("my-host"); const shadowRoot = shadowHost.attachShadow({mode: "open"}); const p = document.createElement("p"); p.innerHTML = "<p>Good evening!</p>"; shadowRoot.appendChild(p); </script>
表示すると下記の様になります。
上記の例では id="my-host" が Shadow Host です。Shadow Host に対して attachShadow() を行うと Shadow Root が作成されます。{ mode: "open" } は、Shadow Root 配下の要素に対して、外部の JavaScript からのアクセスを許可するか否かを指定します。open は許可し、closed は禁止します。Shadow Host の子要素である Good morning! は無視され、Shadow Root の子要素に追加した Good evening! に置換されます。
上記では Shadow Root の子要素を createElement() で p を作成していましたが、下記の様に template 要素のクローンを用いる例が多いようです。cloneNode() の true は子要素もクローンすることを意味しています。
<template id="my-template"> <p>Good evening!</p> </template> <div id="my-host"> <p>Good morning!</p> </div> <script> const shadowHost = document.getElementById("my-host"); const shadowRoot = shadowHost.attachShadow({mode: "open"}); const template = document.getElementById("my-template"); shadowRoot.appendChild(template.content.cloneNode(true)); </script>
Shadow Host の子要素は基本的には無視されますが、slot 属性を指定した要素は、テンプレート内で同じ名前を持つ slot 要素に置換されます。
<template id="my-template">
<slot name="message"></slot> <!-- <p>Good morning!</p> に置換される -->
<p>Good evening!</p>
</template>
<div id="my-host">
<p slot="message">Good morning!</p>
</div>
上記の例では slot="message" の要素が <slot name="message"> 要素に置換され、次の様に表示されます。
複数の Shadow Host を扱うと、例えばテンプレートでデザインを定義し、Shadow Host でカードの中身を定義することができるようになります。
<style> .song-list { display: flex; gap: .5rem; } .song { border: 1px solid #999; padding: .2rem; width: 12rem; } .title { font-weight: bold; } </style> <template id="my-template"> <div class="title"><slot name="title"></slot></div> <div class="artist">(By <slot name="artist"></slot>)</div> </template> <div class="song-list"> <div class="song"> <span slot="title">Let it be</span> <span slot="artist">The Beatles</span> </div> <div class="song"> <span slot="title">Hotel California</span> <span slot="artist">The Eagles</span> </div> <div class="song"> <span slot="title">Stairway to Heaven</span> <span slot="artist">Led Zeppelin</span> </div> </div> <script> const template = document.querySelector("#my-template"); document.querySelectorAll(".song").forEach((e) => { var shadowRoot = e.attachShadow({ mode: "open" }); shadowRoot.appendChild(template.content.cloneNode(true)); }); </script>
上記の例で、.title のスタイルを太字に指定していますが、結果は太字になっていません。Shadow DOM の大きなメリットとして、Shadow DOM の外部で指定したスタイルは、Shadow Host までは指定することができますが、Shadow Root 配下の要素に対しては指定できないという特徴があります。Shadow Root の外側と内側でスタイルの独立性を保つことができます。Shadow Root の内側のスタイルを指定するには下記の様にします。
<template id="my-template"> <style>.title { font-weight: bold; }</style> <div class="title"><slot name="title"></slot></div> <div class="artist">(By <slot name="artist"></slot>)</div> </template>
<slot> と slot="..." を用いて画像も埋め込むことができればよいのですが、画像は下記のような JavaScript で対応する必要があるようです。
<template id="my-template"> <div class="title"><slot name="title"></slot></div> <div class="artist">(By <slot name="artist"></slot>)</div> <div><img class="image" src="#"></div> </template> <div class="song-list"> <div class="song"> <span slot="title">Let it be</span> <span slot="artist">The Beatles</span> <span class="image">./image/let-it-be.jpg</span> </div> </div> <script> const template = document.querySelector("#my-template"); document.querySelectorAll(".song").forEach((e) => { var shadowRoot = e.attachShadow({ mode: "open" }); shadowRoot.appendChild(template.content.cloneNode(true)); shadowRoot.querySelector(".image").src = e.querySelector(".image").innerText; }); </script>
Shadow Host をカスタムエレメントとして定義する方式もよく紹介されています。
<style> my-song-list { display: flex; gap: .5rem; } my-song { border: 1px solid #999; padding: .2rem; width: 12rem; } </style> <template id="my-template"> <style>.title { font-weight: bold; }</style> <div class="title"><slot name="title"></slot></div> <div class="artist">(By <slot name="artist"></slot>)</div> </template> <my-song-list> <my-song> <span slot="title">Let it be</span> <span slot="artist">The Beatles</span> </my-song> <my-song> <span slot="title">Hotel California</span> <span slot="artist">The Eagles</span> </my-song> <my-song> <span slot="title">Stairway to Heaven</span> <span slot="artist">Led Zeppelin</span> </my-song> </my-song-list> <script> const template = document.querySelector("#my-template"); customElements.define("my-song", class MySong extends HTMLElement { constructor() { super(); const shadowRoot = this.attachShadow({ mode: "open" }); shadowRoot.appendChild(template.content.cloneNode(true)); } } ); </script>
template に対して attachShadow() で Shadow Root を作成する方法はクライアントサイトレンダリングではうまく動作しますが、サーバサイドレンダリングの際に画面が一瞬ちらつくなどの問題がありました。これを解決するために、template 要素に shadowrootmode 属性がサポートされ、JavaScript を用いなくても template 要素を Shadow Root 化して埋め込むことが可能となりました。template 要素は自動的に Shadow Root となり、template 要素の親要素を Shadow Host として貼り付けます。複数の Shadow Host を対象として扱うことはできませんが、スタイルの独立性が保たれる要素として扱うことができます。
<my-template>
<template shadowrootmode="open">
<style>.message { font-weight: bold; }</style>
<div class="message">Good morning!</div> <!-- テンプレート外のスタイルの影響を受けない -->
</template>
</my-template>