定義
選択した文字の横や、クリックしたボタンの横に浮かぶ小さな UI(ポップオーバー・ツールバー)が、開いたままページをスクロールすると、元々貼り付いていた場所から離れて見当違いの位置に取り残されるのを直したいときに、この概念を使う。
Fixed-position floating UI scroll drift は、選択・クリック位置に貼り付く UI が position: fixed(画面ビューポート基準で固定され、スクロールしても動かない CSS 配置)** アンカー**(floating UI が貼り付く基準点となる、見えない要素)** element に開くとき一度だけキャプチャした top/left(または getBoundingClientRect、要素の画面上の位置・大きさを測るブラウザ関数)** を保持するとき、ユーザーがスクロールすると本文は動くがアンカーはビューポート(ブラウザで実際に見える画面領域)の過去の座標に残り、ポップオーバー・フォーマットツールバーがハイライトから外れる現象である。
なぜ必要か
Popper・Radix・Floating UI はアンカーの現在の rect(要素の位置・大きさの矩形)に content を整列する。アンカー座標が stale(古くなって実際とずれた状態)だと、ライブラリはそのまま「正確に」誤った位置に従う。selectionchange(ユーザーがテキスト選択領域を変えたときに発生するイベント)はスクロールを発生させないので、選択ベースの UI と scroll 追従は別々に処理しなければならない。
動作原理
- ツールバー open 時に selection/caret 位置を測定して invisible fixed anchor を配置する。
- ユーザーが window または入れ子の
overflow: autoコンテナをスクロールする。 - テキスト・ハイライトは文書座標系で移動するが、anchor の
top/leftは更新されない。 - Floating layer は stale anchor rect を基準に layout される。
scrollイベントは bubble(子→親へイベントが上へ伝播)しないが、capture phase(親→子へ降りながらイベントを先に横取りする段階)で window listener を使えば入れ子のスクローラーまで捕まえられる。
実務での適用
- UI が開いている間だけ scroll(capture) + resize リスナーを登録し cleanup する。
- リンクポップオーバーのように特定の element に貼り付く場合、クリックした DOM ノードの ref を保持し
getBoundingClientRect()で再測定する。ノードが削除されたら閉じる。 - rect が以前と同じなら setState を省略して不要な再レンダーを減らす。
- 長文書で jank(スクロール・アニメーションが滑らかでなく、かくかく途切れる現象)が見えたら
requestAnimationFrame(次の画面描画の直前にコールバックを実行してくれるブラウザ API)throttle(呼び出し頻度を制限)を検討する。 - jsdom テスト: layout API mock +
fireEvent.scroll(window)で座標追従を検証する。
トレードオフ
- scroll ごとの測定は安価な場合が多いが、数千個の同時 open UI では費用が積み上がる — open 状態でのみリスナーを置く。
- capture scroll はすべてのスクローラーで発火するので、ハンドラは軽く保つ。
- 「スクロール時に閉じる」は実装は簡単だが UX が違う — 要件が「追従」なら再測定が正しい。
使ってはいけない場合
- viewport に固定された全域ヘッダー・トースト — 選択アンカーではない。
- スクロールコンテナ外の absolute positioning で、すでに文書座標を追従している場合。
- ネイティブ
position: fixed+ CSSanchor-name(対応環境)でブラウザが追従する最新 API — ポリフィル・ターゲットブラウザの確認が必要。
よくある間違い
selectionchangeだけを聞き scroll は無視。- bubble phase の scroll だけ登録して入れ子スクローラーを miss。
- open 時に一度測定した後、Radix の「バグ」と誤認。
- テスト環境に layout が無く座標が 0 なのに、プロダクションだけで検証。
関連概念
- selectionchange-floating-ui — 選択 UI のトリガー
- radix-portal-scoped-css — Portal・scoped CSS(別個の課題)
- html5-drag-and-drop — mouseup 抑制と選択 UI