Shadow DOMでIslands Architectureっぽく(CSS編)
記事のモチベ
定期的に、既存アプリケーションのリプレースの効率や、既存を残しつつ新規のアプリケーションを導入しやすい方法を考えることがあります。直近Vue2のプロジェクトをReactに変える相談があって今年度例の定期入ったかというモチベでこの記事を書いてます。
前の定期時にはマイクロフロントエンドと向きあってみるというのをアウトプットしてたりしました。
記事の内容
Shadow DOMの特性を使って、既存アプリケーションに対し、異なるWebフロントエンドの実装を入れてみたというものになります。
既存のアプリケーションとShadow DOM内のCSSのスコープが分離できるため、TailwindのようなCSSでも分離して使いやすいのでは?という過程で試してみました。
あと、この記事を書き終えたぐらいのタイミングで正味、今だったらAstro移行するのもありかなとは思いましたが、この記事としては移行用のライブラリ依存(複数アプリを1つのビルドにまとめるようなもの)なしに比較的にWeb標準の機能で徐々に移し替えが可能な手段として記載しました。
今回はCSSにフォーカスした内容となり、別でグローバルステート編も記載したいと思います。
動作環境

1つのページに対し、上からShadow DOM上にあるVue2のアプリケーション、ページ全体を構成しているNext.jsのアプリケーション、Shadow DOM上にあるReactのアプリケーションが動いています。
Next.jsとReactのアプリケーションは同じshadcn/uiで実装したコンポーネントを使用しています。適応するテーマのCSSだけを変えて表示していますが、それぞれのアプリケーションでCSSが干渉していないことがわかります。
解説
それぞれのアプリケーションで展開するためのRoute設定
今回はGitHub Pages上で動かすため、部分的にあまりしない設定していると思います。
Next.jsアプリケーション
-
next.config.tsGitHub Pagesに展開するためSSGで出力next.config.ts
const nextConfig: NextConfig = { output: "export", basePath: "/multi-fw-demo/nextjs", images: { unoptimized: true, }, }; -
/app/multi/page.tsx
/multi-fw-demo/nextjs/multiに今回のサンプルページを作るために設置
Reactアプリケーション
TanStack RouterによるRoute設定
-
src/main.tsxせっかくTanStack Router使っていますがGitHub Pagesに展開されるNext.jsのページがmulti.htmlなので、そこに合わせてRouteを追加しています。main.tsx
let basePath = "/multi-fw-demo/react/"; if (window.location.pathname.startsWith("/multi-fw-demo/nextjs")) { basePath = "/multi-fw-demo/nextjs/"; } const multiRoute = createRoute({ getParentRoute: () => routeTree, path: "multi.html", component: Multi, }); // Create a new router instance const router = createRouter({ routeTree: routeTree.addChildren([multiRoute]), basepath: basePath, });
Vue2アプリケーション
Vue RouterによるRoute設定
-
src/router/index.tssrc/router/index.ts
const routes: Array<RouteConfig> = [ { path: '/multi.html', name: 'MultiFramework', component: HelloWorld }, ] let basePath = '/multi-fw-demo/vue2/'; if (window.location.pathname.startsWith('/multi-fw-demo/nextjs')) { basePath = '/multi-fw-demo/nextjs/'; } if (window.location.pathname.startsWith('/multi-fw-demo/react')) { basePath = '/multi-fw-demo/react/'; } const router = new VueRouter({ mode: 'history', base: basePath, routes })
Shadow DOM上にアプリケーションを展開するためのコード
この2つのコードの違いとしてDOMのid指定がreact-appかvue2-appになっている点ぐらいです。
コードの内容を箇条書きすると
- Shadow Root作成
- Shadow Rootにマウントするためのアプリケーションのid要素追加
- それぞれのアプリケーションで使用するCSS読み込み
- Shadow RootにCSS適応
- 例外的にCSSの@propertyはShadow DOM内に適応されないためグローバルに適応
- アプリケーションのJSファイルを読み込み
の流れになります。
詰まった点
shadcn/uiのテーマがShadow DOM内に適応されない
shadcn/ui公式のCSSのままではShadow DOMにはスタイルの適応ができなく、

:root,
+ :host {
--radius: 0.65rem;
:hostセレクタを追加しました。
Tailwind CSS 4の@propertyルールがShadow DOM内に適応されない
--tw-xxxというような命名規則のあるTailwind CSS共通の@propertyがありますが上書きされて困ることがないのでグローバルに適応するようにしました。

参考
- Tailwind 4の@propertyルールについて
Tailwind CSS 4をShadow DOM内で動作させる方法
ちょっと株式会社(chot-inc.com)のエンジニアブログです。 フロントエンドエンジニア募集中! カジュアル面接申し込みはこちらから chot-inc.com/recruit/iuj62owig
Discussion