
ChatGPT用のブラウザ拡張機能を開発していたとき、ある問題にぶつかりました。チャット内の特定のメッセージまでスクロールする必要があったのですが、私は他の開発者と同じように、まず以下のコードを試しました。
window.scrollY
しかし、結果は毎回ゼロでした。
次に、お決まりのコードを試しました。
element.getBoundingClientRect().top + window.scrollY
それでもうまくいきません。座標はめちゃくちゃで、拡張機能は使い物になりませんでした。
ChatGPTのようなモダンなSPAは、ページ自体をスクロールさせません。<body>は高さ100%に固定されています。スクロールはDOMツリー内のどこかにあるネストされたコンテナ内で発生します。そのため、window.scrollYは永遠に0のままで、計算はサイレントに失敗します。
このデバッグにかなりの時間を費やしてしまいました。しかし、本当の答えは私が思っていたよりもシンプルでした。
ページ自体はスクロールしません。ページ内の何かがスクロールしているのです。それを見つける必要があります。
私がうまくいったアプローチは次のとおりです。まず、対象の要素を取得します。
const el = document.querySelector("div.whitespace-pre-wrap");
次に、実際にスクロールを処理している親要素を見つけるまで、DOMツリーを遡ります。
function findScrollParent(element) {
let parent = element.parentElement;
while (parent) {
const style = getComputedStyle(parent);
if (/(auto|scroll)/.test(style.overflowY)) return parent;
parent = parent.parentElement;
}
return document.documentElement;
}
const container = findScrollParent(el);
この関数は、各親要素の計算されたoverflowYスタイルをチェックします。autoまたはscrollを持つ最初の要素がスクロールコンテナです。ChatGPTでは、通常、.overflow-y-autoのようなクラスを持つ<div>がこれに該当します。
コンテナが見つかれば、実際のY座標の計算は簡単です。
const y = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
要素の位置からコンテナのオフセットを減算し、コンテナがスクロールした距離を足し戻します。これにより、要素のコンテナのトップに対する相対位置が得られます。これこそが、そもそもあなたが求めていたものです。
計算したY座標に赤い線を描画して、完全に一致するか確認してください。
const marker = document.createElement("div");
marker.style.cssText = `position: absolute; left: 0; right: 0; background: red; height: 2px; top: ${y}px`;
document.body.appendChild(marker);
線がターゲット要素の真上に表示されれば、正しく計算できています。
これは偶然ではありません。ReactのようなフレームワークやTailwindのようなライブラリは、フルハイトレイアウトを推奨しています。パターンはシンプルです。ルート要素がビューポート全体を占め、各セクションが独自のオーバーフローを管理します。
考えてみてください。ページ全体がスクロールする場合、ナビゲーションバー、サイドバー、モーダルはスクロールと競合します。下にスクロールするとサイドバーは消えてしまいます。固定ヘッダーは、その場に留まるために追加のCSSハックが必要になります。スクロールコンテナをネストすることで、UIの各セクションが独立性を保ちます。
ChatGPTはこれを極端に推し進めています。サイドバーは独立してスクロールします。チャットペインは独立してスクロールします。設定パネルも独立してスクロールします。これは合理的な設計上の選択ですが、ウェブの初期から存在していた前提を打ち破るものです。
GitHub、Discord、Linear、Notion、そしてほとんどすべてのReactベースのSPAで、同じ問題に遭遇するでしょう。このパターンを知っていれば、どこでもそれを見かけるようになるはずです。
私はブラウザ拡張機能を開発しているときにこれに遭遇しました。しかし、SPAで正確なY座標が必要になる他のケースもあります。
PlaywrightやCypressのようなUI自動化ツールは、要素を操作する前に要素までスクロールする必要があることがよくあります。スクロール検出にwindow.scrollYを使用している場合、同じように失敗します。
カスタムツールチップやポップオーバーの配置は、間違ったコンテナに対して位置を計算すると壊れます。ドロップダウンが本来あるべき場所から2000ピクセルも上にレンダリングされているのを見たことがあります。
現在表示されているセクションをハイライトするためのスクロールスパイロジック。実際に何が見えているかを知るには、真のコンテナが必要です。
フルページコンテンツをキャプチャするスクリーンショットツール。ツールがネストされたスクロールコンテナについて知らない場合、ビューポートに収まる部分のスクリーンショットしか得られません。
この問題に直面しているかどうかをすばやく確認する方法は次のとおりです。
// If this returns 0, you've got a nested scroll container
console.log("scrollY:", window.scrollY, "body height:", document.body.scrollHeight);
ChatGPTのようなアプリでwindow.scrollYが0を返すのはなぜですか?
ネストされたスクロールコンテナ内の要素のY座標の正しい計算式は何ですか?
ChatGPTはストリーミングアプリです。ページを見ている間に新しいメッセージが表示されます。要素が追加、削除、移動されます。
特定の要素の位置を時間の経過とともに追跡している場合、一度計算して値をキャッシュしないでください。スクロール時とDOM変更時に再計算してください。スクロールイベントリスナーと組み合わせたMutationObserverは、両方のケースをカバーします。
const observer = new MutationObserver(() => {
const y = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
// do something with the updated y
});
observer.observe(container, { childList: true, subtree: true });
レイアウトがシフトする場合、コンテナ上のResizeObserverも役立ちます。私は、両方のオブザーバーをスクロールハンドラーのスロットルと組み合わせることで、遭遇したすべてのエッジケースをカバーできることを見出しました。
これはあなたが思っている以上に重要です。ChatGPTは、上にスクロールするとメッセージを遅延ロードします。追跡している要素の上に新しいコンテンツがロードされると、その位置が変わります。オブザーバーがないと、古い座標を指すことになります。
MutationObserverは頻繁に発火します。コールバック内で位置を測定している場合は、スロットルをかけてください。そうしないと、UIのスクロールパフォーマンスが低下し、スクロールの途切れほどデモを台無しにするものはありません。
覚えておくべき3つのこと:
window.scrollYは、document自体がスクロールする場合にのみ機能します。SPAでは、通常そうではありません。overflowYをチェックして真のコンテナを見つけます。getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTopで位置を計算します。それだけです。魔法はありません。フレームワークのトリックもありません。モダンなSPAでDOMが実際にどのように機能するかを理解するだけです。windowがスクロールターゲットであるという仮定をやめた途端、すべてがうまくいくようになりました。
AI CLIログの整形、Cron式の作成、JWTの解析、chmod権限の計算をオフラインで実行。

Next.jsのハイドレーションエラーを解決する2026年版完全ガイドで、Minified React Error #418、Error #423、Text Content Mismatch、window/localStorageのSSRバグをコピペコードとライブデバッガーで修正しましょう。
続きを読む
ReactForgetとReactコンパイラの内部動作をAST変換、データフロー解析、そしてuseMemoを削除できる理由を理解することで詳細に解説します。
続きを読む
Next.js14でServer Actions、useFormState、useFormStatus、Zodを使ったフォーム管理を詳しく解説し、クライアントサイドJavaScriptの負担なく、モダンでプログレッシブエンハンスメントなフォームを構築する方法を学びましょう。
続きを読む