WXT(Web Extension Toolkit)でShadow DOM のレイヤーを任意要素に重ねる
概要
今回は WXT を使って最小構成の拡張機能を作成し、Content Script で Shadow DOM のレイヤーを任意要素に重ねるまでを試してみました。インストール時の特定バージョンでのエラーや、オプションの比較なども行なっているので参考になれば嬉しいです。
WXTとは?
WXTは、Web拡張機能を構築するための最新のオープンソースフレームワークです。Nuxtに触発され、以下の目標を掲げています。
- 素晴らしいDXを提供する
- すべての主要ブラウザにファーストクラスのサポートを提供します
特徴
- サポートブラウザ
- Chrome, Firefox, Edge, SafariなどのChromiumベースのブラウザ用拡張機能を構築
- MV2とMV3
- 同じコードベースで任意のブラウザ用のManifest V2 / V3 を構築
- HMRサポート
- ファイルベースのエントリポイント
- TypeScriptがデフォルト
- 自動インポート
- unimport を使用した自動インポートサポート
- 自動公開
プロジェクト作成
今回は wxt-example という名前でプロジェクトを作成して進めて行きたいと思います。
$ mkdir wxt-example
$ cd wxt-example
$ pnpm --version
10.18.3
$ pnpm init
※ 今回 wxt@0.20.11 のバグに遭遇したので package.json に以下を追加してインストール実施してます。今後バグが解消されれば pnpm i -D wxt で済む話かもしれません (バッドノウハウ参照)
package.json に以下を追加
{
"devDependencies": {
"wxt": "0.20.11"
},
"pnpm": {
"overrides": {
"@wxt-dev/storage": "1.2.0"
}
}
}
pnpm install 実施し、entrypoints/background.ts を以下内容で作成します。
export default defineBackground(() => {
console.log("Hello world!");
});
package.json の scripts に以下を追加します。
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build": "wxt build",
"build:firefox": "wxt build -b firefox",
"zip": "wxt zip",
"zip:firefox": "wxt zip -b firefox",
"postinstall": "wxt prepare"
},
pnpm dev を実施すると Chrome が起動され拡張機能がインストールされている状態になっていればOKです。

※ コンソールログを見る場合は右上の「デベロッパーモード」をONにして「ビューを検証 Service Worker」のリンクを開くと見れます。
簡単なサンプル実装
今回は特定のサイト上でとある要素の上に被せるLayerを追加する様なサンプルを作成してみたいと思います。実装するにあたり以下のExampleを参考にしました。
Entrypoints
WXTでは機能毎に👆のリンク先にある様な複数のエントリポイントが用意されています。今回はこの中の Content Scripts を使用します。Content Scripts では以下のUIを差し込むメソッドが用意されています。
-
Integrated
createIntegratedUi- コンテンツが直接差し込まれて、ページのCSSの影響を受けます
-
Shadow Root
createShadowRootUi- Shadow DOMとして差し込まれます
-
IFrame
createIframeUi- iframeとして差し込まれます
今回は Shadow Root を使ってLayerを差し込むようにしてみたいと思います。
実装
新規に entrypoints/content.ts を以下内容で作成します。
export default defineContentScript({
matches: ["*://*/*"],
cssInjectionMode: "ui",
async main(ctx) {
const ui = await createShadowRootUi(ctx, {
name: "example-overlay",
position: "inline",
anchor: ".main",
onMount(container) {
const app = document.createElement("p");
app.textContent = "Hello world!";
app.style.color = "red";
container.append(app);
},
});
ui.mount();
},
});
-
cssInjectionMode
- 差し込まれた要素のスタイルをコンテンツスクリプトに挿入する方法をカスタマイズします
- manifest
- マニフェスト内のコンテンツ スクリプトの
css配列の下に CSS を含めます
- マニフェスト内のコンテンツ スクリプトの
- manual
- マニフェストからCSSを除外します。ページに手動で読み込む必要があります
- ui
- マニフェストからCSSを除外します。CSSは、呼び出し時にUIに自動的に追加されます
- manifest
- 差し込まれた要素のスタイルをコンテンツスクリプトに挿入する方法をカスタマイズします
-
position
- 以下の3種類の中から設定します
inlineoverlaymodal
- 以下の3種類の中から設定します
-
anchor
- CSSセレクタ、XPath式、要素、またはこれら3つのいずれかを返す関数を渡す
- ページ内のUI追加位置を決定する
今回は https://wxt.dev/ ページの .main 要素に対して赤字で Hello world! の文字を position を変えて実行してみました。
-
inline

- 差し込まれたShadow DOM
- 差し込まれたShadow DOM
-
overlay

- 差し込まれたShadow DOM
- 差し込まれたShadow DOM
-
modal

- 差し込まれたShadow DOM
- 差し込まれたShadow DOM
差し込まれたShadow DOM を比較してみるとpositionの違いが一目瞭然ですね!
Layer追加
次に .main 要素の上に背景色がついたLayerを重ねて表示してみたいと思います。先ほどの entrypoints/content.ts を以下に修正します。
export default defineContentScript({
matches: ["*://*/*"],
cssInjectionMode: "ui",
async main(ctx) {
const ui = await createShadowRootUi(ctx, {
name: "example-overlay",
position: "inline",
anchor: ".main",
append: "last",
css: `
:host{
display: block !important;
position: absolute !important;
inset: 0 !important;
}
html, body { width: 100%; height: 100%; margin: 0; }
#layer { width: 100%; height: 100%; background: red; opacity: 0.5; }
`,
onMount(container) {
const root = document.createElement("div");
root.id = "layer";
container.append(root);
// もし .mainが position:relative でない場合は付与
const main = document.querySelector(".main");
if (main) {
(main as HTMLElement).style.position = "relative";
}
},
});
ui.mount();
},
});
👆ではoptionのcssで差し込まれた要素を、親要素のサイズ一杯に広げて表示するようにしてます。この時に親要素が position: relative である必要があります。あとは id: layer の div 要素のスタイルも設定してます。
実行すると👇のように .main 要素の上にLayerが被さって表示されている見た目になります。

Loadingなどで遅れて表示されるパターン
次にとある要素がLoadingなどで遅れて表示されるパターンの場合を試してみたいと思います。
分かりづらいかもですが、👇zennの筆者の記事一覧の全Cardの上に同じく赤い半透明のLayerを被せる様にしてみたいと思います。

👆は既に先ほどの実装で全CardにLayerを被せるようにしているのですが、Loadingが挟むので anchor で指定した要素が見つからずうまくいきません。
この対応はそんな難しい事はなく ui.mount(); の部分で autoMount を使うようにしてあげます。名前の通りアンカーが動的に追加/削除される際に、UIを自動的にマウントおよび削除してくれます。
export default defineContentScript({
// ...
async main(ctx) {
const ui = await createShadowRootUi(ctx, {
// ...
});
ui.autoMount(); // ← mount() ではなく autoMount() を呼ぶ
},
});
👇実行結果

まとめ
今回はたまたまインストール時のエラーでハマりましたが、それ以外は 素晴らしいDXを提供する と謳っているだけありサクッと開発に着手できる体験は良かったです。また今後別の機能も色々試してみたいと思います。
バッドノウハウ
pnpm i -D wx 時に以下のエラーが発生する
ERR_PNPM_WORKSPACE_PKG_NOT_FOUND In : "@wxt-dev/browser@workspace:^" is in the dependencies but no package named "@wxt-dev/browser" is present in the workspace
This error happened while installing the dependencies of wxt@0.20.11
at @wxt-dev/storage@1.2.5
👆のIssueによると wxt@0.20.11 が依存している @wxt-dev/storage@1.2.5 が原因でエラーが起きているよう。wxtのバージョンを下げるか、本記事でもやっている様に @wxt-dev/storage を 1.2.0 でoverrideする。
参考URL
Discussion