CSS-in-JS の動的スタイルを Document Picture-in-Picture に反映する
PKSHA Technology で SWE をしている今泉です。今回は、Document Picture-in-Picture を使った機能の実装時に遭遇したスタイルの問題を解決する方法を紹介します。
背景
MUI を使用した React アプリケーションで Document Picture-in-Picture API(以下、PiP)を実装した際、PiP ウィンドウ内で表示したコンポーネントにスタイルがうまく適用されない問題に遭遇しました。
調査の結果、PiP ウィンドウは独立した Document コンテキストを持つため、メインウィンドウのスタイルを引き継げないことが原因だと分かりました。
関連知識
MUI と Emotion
MUI (Material-UI) は、内部でスタイリングライブラリとして Emotion を使用しています[1]。Emotion は、JavaScript でスタイルを記述できる CSS-in-JS ライブラリ[2]です。
PiP ウィンドウと Document コンテキスト
Document Picture-in-Picture API で開かれるウィンドウは、メインウィンドウとは独立した Document コンテキストを持ちます。Emotion で動的に生成されたスタイルはメインウィンドウの <head> タグ内に挿入されていました。しかし、PiP ウィンドウではそのスタイルが読み込まれないため、表示が崩れてしまっていました。
スタイルシートコピーによる対処の限界
Chrome 公式ドキュメント[3]では、以下のようにスタイルシートを PiP ウィンドウにコピーする方法が紹介されています。
[...document.styleSheets].forEach((styleSheet) => {
try {
const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join('');
const style = document.createElement('style');
style.textContent = cssRules;
pipWindow.document.head.appendChild(style);
} catch (e) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = styleSheet.type;
link.media = styleSheet.media;
link.href = styleSheet.href;
pipWindow.document.head.appendChild(link);
}
});
この方法を試したところ、静的なスタイルシートは正しくコピーされましたが、CSS-in-JS で動的に生成されるスタイルには対応できませんでした。具体的には以下の問題が発生しました。
- コンポーネントのレンダリング時に生成されるスタイルが反映されない
- 状態変化やホバーなど、実行時に追加されるスタイルが反映されない
MutationObserver[4] で document.head 内のスタイル要素の追加・変更を監視して逐次コピーするようにしても、タイムラグが発生したり反映されないことがありました。
解決策:Emotion Cache Provider の使用
Emotion のCacheProvider[5]を使用してスタイルの挿入先を PiP ウィンドウに指定することで、この問題を解決できました。
サンプルコード
import { useState } from 'react';
import { CacheProvider } from '@emotion/react';
import createCache from '@emotion/cache';
import { createPortal } from 'react-dom';
// PiPウィンドウ用コンポーネント
function PiPWindow({ pipWindow, children }) {
const cache = createCache({
key: 'pip',
container: pipWindow.document.head,
prepend: true,
});
return createPortal(
<CacheProvider value={cache}>{children}</CacheProvider>,
pipWindow.document.body
);
}
// 使用例
function App() {
const [pipWindow, setPipWindow] = useState(null);
const openPiP = async () => {
// PiPウィンドウを開く
const newPipWindow = await documentPictureInPicture.requestWindow();
// 静的スタイルシート(外部CSS、フォントなど)をコピー
[...document.styleSheets].forEach((styleSheet) => {
try {
const cssRules = [...styleSheet.cssRules]
.map((rule) => rule.cssText)
.join('');
const style = document.createElement('style');
style.textContent = cssRules;
newPipWindow.document.head.appendChild(style);
} catch {
if (styleSheet.href) {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = styleSheet.type;
link.media = styleSheet.media.toString();
link.href = styleSheet.href;
newPipWindow.document.head.appendChild(link);
}
}
});
setPipWindow(newPipWindow);
};
return (
<>
<button onClick={openPiP}>PiPを開く</button>
{pipWindow && (
<PiPWindow pipWindow={pipWindow}>
<YourComponent />
</PiPWindow>
)}
</>
);
}
Emotion は通常、document.head にスタイルを挿入しますが、CacheProvider を使用することで、スタイルの挿入先を任意の DOM 要素に変更できます。この仕組みを利用して、PiP ウィンドウの document.head にスタイルを直接生成することで、動的なスタイルも含めてすべてのスタイルが正しく適用されました。
まとめ
Document Picture-in-Picture に MUI のスタイルを反映するため、Emotion の CacheProvider でスタイルの挿入先を PiP ウィンドウに指定しました。この方法により、動的なスタイルをタイムラグなく適用できるようになりました。
なお、Document Picture-in-Picture は Chrome/Chromium ベースのブラウザでサポートされ始めた実験的な機能です[6]。ブラウザのサポート状況を確認しながら使用することをお勧めします。
-
https://mui.com/material-ui/integrations/interoperability/ ↩︎
-
https://developer.chrome.com/docs/web-platform/document-picture-in-picture?hl=ja#copy_style_sheets_to_the_picture-in-picture_window ↩︎
-
https://developer.mozilla.org/ja/docs/Web/API/MutationObserver ↩︎
-
https://developer.mozilla.org/en-US/docs/Web/API/Document_Picture-in-Picture_API#browser_compatibility ↩︎
Discussion