How to Get the Correct Y Position of an Element in ChatGPT UI (2026 Guide)

The Problem
I was building a browser extension for ChatGPT and hit a wall. I needed to scroll to a specific message in the chat, so I did what any developer would do:
window.scrollY
Zero. Every single time.
I tried the classic formula:
element.getBoundingClientRect().top + window.scrollY
Still broken. My coordinates were garbage, and my extension was useless.
Why is it always 0?
Modern SPAs like ChatGPT don't scroll the page. The <body> has a fixed height of 100%. Scrolling happens inside a nested container somewhere in the DOM tree. So window.scrollY sits at 0 forever, and your calculations fail silently.
I spent way too long debugging this. The real answer is simpler than I thought.
Finding the Real Scroll Container
The page doesn't scroll. Something inside the page does. You just need to find it.
Here's the approach that worked for me. First, grab the element you care about:
const el = document.querySelector("div.whitespace-pre-wrap");
Then walk up the DOM tree until you find the parent that actually handles the scrolling:
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);
This function checks each parent's computed overflowY style. The first one with auto or scroll is your scroll container. In ChatGPT, that's usually a <div> with a class like .overflow-y-auto or something similar.
Once you have the container, calculating the real Y position is straightforward:
const y = el.getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop;
You're subtracting the container's offset from the element's position and adding back how far the container has scrolled. That gives you the element's position relative to the container's top, which is what you actually wanted in the first place.
Test It Visually
Draw a red line at your calculated Y to make sure it lines up perfectly:
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);
If the line sits right on top of your target element, you got it right.
Why Do SPAs Do This?
It's not an accident. Frameworks like React and libraries like Tailwind encourage full-height layouts. The pattern is simple: the root element takes the full viewport, and each section manages its own overflow.
Think about it. If the whole page scrolls, then navigation bars, sidebars, and modals fight against that scroll. A sidebar scrolls away when you scroll down. A sticky header needs extra CSS hacks to stay put. By nesting scroll containers, each section of the UI stays independent.
ChatGPT takes this to an extreme. The sidebar scrolls independently. The chat pane scrolls independently. The settings panel scrolls independently. It's a reasonable design choice, but it breaks assumptions that have held since the early days of the web.
This Isn't Just ChatGPT
You'll hit this same problem in GitHub, Discord, Linear, Notion, and pretty much any React-based SPA. Once you know the pattern, you'll start seeing it everywhere.
When You Actually Need This
I ran into this while building a browser extension. But there are other cases where you might need accurate Y coordinates in an SPA.
UI automation tools like Playwright and Cypress often need to scroll to elements before interacting with them. If their scroll detection uses window.scrollY, they'll fail in the same way.
Custom tooltip or popover positioning breaks when you calculate positions relative to the wrong container. I've seen dropdowns render 2000 pixels above where they should be.
Scroll spy logic for highlighting the current section in view. You need the real container to know what's actually visible.
Screenshot tools that capture full-page content. If the tool doesn't know about nested scroll containers, you'll get a screenshot of whatever fits in the viewport and nothing more.
Here's a quick check to know if you're dealing with this problem:
// If this returns 0, you've got a nested scroll container
console.log("scrollY:", window.scrollY, "body height:", document.body.scrollHeight);
What About Dynamic Content?
ChatGPT is a streaming app. New messages appear while you're looking at the page. Elements get added, removed, and shifted around.
If you're tracking a specific element's position over time, don't calculate it once and cache the value. Recalculate on scroll and on DOM changes. A MutationObserver paired with a scroll event listener covers both cases:
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 });
A ResizeObserver on the container is also useful if the layout shifts. I've found that combining both observers with a throttle on the scroll handler covers every edge case I've encountered.
This matters more than you think. ChatGPT loads messages lazily as you scroll up. When new content loads above your tracked element, its position changes. Without the observer, you'd be pointing at stale coordinates.
Performance Warning
MutationObserver fires a lot. If you're measuring positions inside the callback, throttle it. Otherwise you'll tank the UI's scroll performance, and nothing ruins a demo like stuttering scroll.
The Short Version
Three things to remember:
window.scrollYonly works when thedocumentitself scrolls. In SPAs, that's usually not the case.- Find the real container by walking up the DOM and checking
overflowY. - Calculate positions with
getBoundingClientRect().top - container.getBoundingClientRect().top + container.scrollTop.
That's it. No magic. No framework tricks. Just understanding how the DOM actually works under modern SPAs. Once I stopped assuming window was the scroll target, everything clicked into place.
You Might Also Like
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

TypeScript Generics: Advanced Patterns for Type-Safe APIs
Master advanced TypeScript generics patterns: conditional types, mapped types, distributive infer constraints, and building type-safe production libraries.
Read more
Intersection Observer vs getBoundingClientRect in JavaScript: Performance Deep Dive
Detailed performance comparison between Intersection Observer and getBoundingClientRect for scroll tracking, lazy loading, and viewport detection.
Read more
JavaScript to Luau: Roblox Scripting for Web Developers
Learn Roblox scripting coming from JavaScript and TypeScript: Complete syntax mapping table, 1-based indexing, coroutines, and roblox-ts development.
Read more