[発端] Top Layerの登場
登場してからしばらく経ち、最近は積極的に使っているTopLayerというものがある。
Top layer - Glossary | MDN
The top layer is a specific layer that spans the entire width and height of the viewport and sits on top of all other layers displayed in a web document. It is created by the browser to contain elements that should appear on top of all other content on the page.
https://developer.mozilla.org/en-US/docs/Glossary/Top_layer
dialogタグやpopover APIなどで、特定の要素を現在のすべての要素の上に表示できる機能である。複数呼び出すと、呼び出した順にスタックされる。
z-index地獄から救ってくれる、まさに光のような機能だ。
そのため、最近の開発ではモーダルにはdialogを積極的に使い、popoverやdropdownのような要素にはpopover APIを使っている。
ただし、開閉の仕組みは以下のように命令的に使う形で実装されている。
dialog.showModal();
dialog.close();
そこで、overlay-kitのような宣言的にoverlayを扱うツールとも上手く連携できるように、それなりに整えて使っている。
const SomeDialog = ({ onClose, children, ...rest }) => {
const dialogRef = useRef<HTMLDialogElement | null>(null);
return (
<dialog
ref={(node) => {
if (!node) return;
dialogRef.current = node;
node.showModal();
// マウント時に開くように実装
}}
onClick={(e) => {
if (e.target instanceof HTMLElement && e.target.nodeName === "DIALOG") {
ref.current?.close();
}
}}
{...rest}
>
{children}
</dialog>
);
};
const SomeButton = ()=>{
const handleClick = ()=>{
overlay.open(({ unmount })=> <SomeDialog onClose={unmount}>)
}
return <button onClick={handleClick}>ボタン</button>
}
このように使っている。とはいえ、TopLayerの活用方法について語りたいわけではなく、TopLayerをReactで使う際に遭遇する問題がある。
[展開/クライマックス] 問題の発見
React自体の問題というよりは、React DevToolsの問題である。
DevToolsにはビューポートに表示される2つの機能がある。
- Reactレンダリングハイライト
- Reactコンポーネントインスペクタ
レンダリングハイライトは、レンダリングされるコンポーネントをハイライトする機能であり、


インスペクタは、対象のコンポーネントの上にオーバーレイを表示して情報を見せる機能である。

これらの機能が、TopLayerの導入によって問題が発生するようになった。
これらの機能は、高いz-indexを持つcanvasの上に描画する形で実装されている。
// packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js
function initialize(): void {
canvas = window.document.createElement("canvas");
canvas.style.cssText = `
xx-background-color: red;
xx-opacity: 0.5;
bottom: 0;
left: 0;
pointer-events: none;
position: fixed;
right: 0;
top: 0;
z-index: 1000000000;
`;
const root = window.document.documentElement;
root.insertBefore(canvas, root.firstChild);
}
従来の環境では大抵のz-indexの上に位置していたが、TopLayerの導入により、最上位に位置できなくなる問題が発生した。

上の映像でも見られるように、Dialogの背後でハイライトが更新されているが、DialogはTopLayerに位置しているため表示されない。
React-DevTools側を修正する必要があるが、これをどう解決できるだろうか。
[下降] 解決方法
作業自体はすでに完了しているので、該当のPRで確認できる。
[DevTools] Use Popover API for TraceUpdates highlighting by yongsk0066 · Pull Request #32614 · facebook/react
Summary When using React DevTools to highlight component updates, the highlights would sometimes appear behind elements that use the browser&#39;s top-layer (such as &lt;dialog&gt; elements or comp...
https://github.com/facebook/react/pull/32614/[DevTools] Use Popover API for component inspect overlays by yongsk0066 · Pull Request #32703 · facebook/react
Following the Popover API integration in #32614, this PR adds the same capability to Component Inspector highlighting in React DevTools. Summary When selecting a component in the Components tab, th...
https://github.com/facebook/react/pull/32703まず検討した要件は以下の通りである。
- highlight、inspectorをTopLayerに表示する必要がある。
- ユーザーのアクションやスタイルに干渉してはならない。
- DevToolsの要素を除く、他のTopLayer要素よりも上位に位置する必要がある。
1. highlight、inspectorをTopLayerに表示する
このために、DevToolsのキャンバスをDialogで囲んで表示するか検討したが、popover APIを使う方が構造への影響が少ない方向だと判断し、こちらで進めることにした。
この実装には、canvasにpopover属性を付与し、DOMに追加された後にshowPopover関数を呼び出すだけでよい。
// packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js
// ...
function initialize(): void {
canvas = window.document.createElement('canvas');
canvas.setAttribute('popover', 'manual');
// $FlowFixMe[incompatible-use]: Flow doesn't recognize Popover API
canvas.style.cssText = `
xx-background-color: red;
xx-opacity: 0.5;
position: fixed;
right: 0;
top: 0;
background-color: transparent;
outline: none;
box-shadow: none;
border: none;
`;
// ...
canvas.showPopover();
2. ユーザーのアクションやスタイルに干渉してはならない
dialogやpopoverには基本的にEscキーで閉じるなどの指定されたアクションがある。そのため、単純にpopoverを使うと、ユーザーがEscを押した際に、意図したTopLayer要素ではなく、DevToolsのハイライトが先にイベントを受け取って閉じてしまう不具合が発生し得る。
これを防ぐために、popoverをmanual属性に設定して干渉しないようにした。
スタイルについても同様に、デフォルトスタイルを除去した。
3. DevToolsの要素を除く、他のTopLayer要素よりも上位に位置する
初回の実装では気づかず、レビュー過程で発見したケースである。TraceUpdateの仕組みを見ると、レンダリングのたびにキャンバスが再生成されるのではなく、その上に更新される形で動作する。 そして認識のために一定時間残り続ける。そのため、素早くDialogを閉じて再度開いた場合、DevToolsのハイライトはTopLayerに残存したまま優先順位で押し出され、再び背後に表示される状況が発生する。
これを防ぐには、popoverの更新タイミングを調整する必要がある。ハイライトが表示されるタイミングであるdrawWebメソッド側で調整しなければならない。
// packages/react-devtools-shared/src/backend/views/TraceUpdates/canvas.js
// ...
function drawWeb(nodeToData: Map<HostInstance, Data>) {
if (canvas === null) {
initialize();
}
// ...
if (canvas !== null) {
if (nodeToData.size === 0 && canvas.matches(':popover-open')) {
canvas.hidePopover();
return;
}
if (canvas.matches(':popover-open')) {
canvas.hidePopover();
}
canvas.showPopover();
}
レンダリングされた要素を描画する際にhidePopoverしてから再度すぐに開く形で実装した。
こうすることで、再描画のたびにTopLayerに再追加されるため、TopLayer上での優先順位を維持できるようになる。

Inspector Overlay側でも同様の実装を行った。
[結末] マージを待つ
この機能を追加したビルドでテストを兼ねて一週間ほど実際の業務で使用してみたが、大きな問題なく満足して使うことができた。
実際に使ってみたい場合は、該当PRのブランチを取得してビルドすれば使用できる。
これらのPRはまだマージされていない。
投稿してから2週間ほど経っており、メンテナーと一週間ほどのやり取りがあったが、思ったより好意的な反応だったので、マージされるのをただ待っていた。
メンテナーが自分のコードが気に入らなくて別途作っているのではないかとヒヤヒヤもしたが、GPTとの深い心理カウンセリングを経て回復し、あまり気にしないことにした。
とにかく、必要な機能を自分で追加して使った意味のある経験だったので、短くではあるが記録として残すことにした。
[アップデート] 2025.05.07
機能がupstreamにマージされた!DevToolsのデプロイはまだ行われていないが、コミットがReactに反映されたことが嬉しい。
[番外] Flowの型問題
PRの内容を見ると、Flow関連のignoreコメントが多いが、これはFlowではまだpopover関連のプロパティが定義されていないため、型エラーが発生するからである。
React Compilerのようなコードはtypescriptで書かれているが、React自体やDevToolsはFlowで型チェックを行っている。
セットアップなしにReactリポジトリをクローンすると、JSファイルなのに型が書かれていて赤線が大量に表示されるのは、Flowベースだからである。
これはFlow LSPをインストールし、型チェックを実行すれば解消される。
いつかFlowFixMeもなくなることを願って、Flow側にもpopover関連の型を直接追加してみた。
Add Popover API to DOM environment definitions by yongsk0066 · Pull Request #4607 · flow-typed/flow-typed
Links to documentation: https://developer.mozilla.org/en-US/docs/Web/API/Popover_API Link to GitHub or NPM: https://html.spec.whatwg.org/multipage/popover.html https://html.spec.whatwg.org/mult...
https://github.com/flow-typed/flow-typed/pull/4607この作業は思ったより早く一発でマージされた。
ReactのFlowバージョンが低いためすぐにコメントが削除されることはないが、いつかバージョンが上がれば、コメントも削除できるだろう。
あるいは、まさかその間にTypeScriptへの移行が起きることは…ないだろうが?