position:fixedを将来の変更に対しても確実にビューポート固定にしたいならReactportalでdocument.body直下へ逃がす
CSSのposition: fixed要素は、その祖先のいずれかがtransform・filter・backdrop-filter・will-change: transformのいずれかを持つと、その祖先が新たな containing block になり、ビューポートではなくその祖先基準で位置やサイズが決まるようになる(CSS仕様上の振る舞い)。パン/ズームで使う側のキャンバスなどでこの transform が後から付与されると、それまでviewportに完全固定されていたはずのボタン群が、その transform につられて一緒に動くように見えるバグが発生する。
適用条件: position: fixedで常に画面固定していてほしいUI(ズームボタン・FAB・固定ヘッダー等)を、将来変更されうるJSXツリーのどこか(たとえ今は直接のtransform祖先がなくても)に置く場合。現在のツリー構造を目視確認して問題なさそうでも、リファクタで上位に transform が入ると容易に壊れるので、構造目視確認だけでは不十分。
対処方法: ReactならcreatePortal(<FixedUi/>, document.body)でdocument.body直下へレンダリングする。document.body自体にtransformが付くことはまずないため、「JSX上の親子関係が何であれ、DOM上は常にbody直下」という保証が構造的に成立し、将来のリファクタでも崩れなくなる。
確認方法: ブラウザのelement.parentElement.tagNameを確認してBODYになっていることでportal化を実証できる。加えて、対象のパン/ズーム操作を実行しの前後でgetBoundingClientRect()(またはPlaywrightのboundingBox())が完全に一致することをアサーションすれば、回帰を自動テスト化できる。
教訓: 「現在のツリーを目視確認してtransform祖先がなければ安全」という判断は、その瞬間の構造にしか通用しない。「position: fixedのUIは将来も確実にビューポート基準であり続けるべきか」を問うなら、構造の目視ではなくportalで仕様として保証する方が安価。