skip to content

In a browser MouseEvent, what do screenX, clientX, pageX and offsetX each measure, and how do you reliably get the pointer position relative to a specific element?

level: juniorimportance: should knowfreq 55%

answer

  1. four origins, one point
  2. viewport versus document
  3. scroll offset is the difference
  4. target's padding edge surprises delegation
  5. clientX minus getBoundingClientRect().left

basics

~20 s

They differ only in origin: screenX is measured from the screen, clientX from the viewport, pageX from the document (clientX plus scroll offset), and offsetX from the padding edge of the event target. For element-local coordinates, subtract getBoundingClientRect().left from clientX.

solid answer

~50 s

All four are horizontal positions with different origins. `screenX` is relative to the user's screen, so it changes when the window moves. `clientX` is relative to the viewport's top-left corner and does not include scrolling. `pageX` is relative to the document, so it equals `clientX + window.scrollX` and keeps growing as you scroll down a long page. `offsetX` is relative to the padding edge of `event.target` — which, because the target is the deepest element under the pointer, is often a child you did not expect when you use event delegation. For "where in this box did the user click?" the dependable recipe is `const rect = el.getBoundingClientRect(); const x = event.clientX - rect.left;` — both values are viewport-based, so they stay consistent under scrolling, and `getBoundingClientRect()` also reflects CSS transforms. `PointerEvent` inherits all of these from `MouseEvent`.

go deeper

for a junior

Be able to say plainly that clientX is viewport-relative and pageX is document-relative, and that the two differ by the scroll offset. Knowing the clientX-minus-rect recipe is enough to pass this one.

for a middle

Explain why offsetX is measured from the event target's padding edge and why that makes it unreliable under event delegation, and derive pageX from clientX plus window.scrollX out loud.

for a senior

Show the judgment of picking one coordinate space and staying in it across a whole drag implementation, and mention what CSS transforms and scrolling containers do to older offsetParent-based maths.

for a principal

Frame it as an API-design decision for a shared interaction utility: expose element-local coordinates so consumers never touch raw event properties, and document the transform and zoom assumptions the utility makes.

## The one thing that differs: the origin Every pointer position the DOM gives you is the *same physical point*, expressed against a different origin. Nothing about the four property pairs is mysterious once you fix the origin in your head. Each comes as an X/Y pair on `MouseEvent`, and `PointerEvent` inherits every one of them because `PointerEvent extends MouseEvent`. ## screenX / screenY — origin: the screen Measured from the top-left of the user's screen. These change when the user moves the browser window, and they are essentially never what you want inside a page. Their legitimate use is positioning something relative to the physical display (multi-monitor tooling, or comparing against `window.screenX`). ## clientX / clientY — origin: the viewport Measured from the top-left corner of the visible viewport — the area the page is rendered into, excluding browser chrome. Crucially, **`clientX` does not include scroll**. If an element is on screen at the same visual spot, `clientX` reports the same number whether the page is scrolled to the top or 4000px down. This is the coordinate space that `getBoundingClientRect()` also uses, which is exactly why the two compose so well. ## pageX / pageY — origin: the document Measured from the top-left of the whole document, so scrolling *is* included: ```js event.pageX === event.clientX + window.scrollX; // true (modulo sub-pixel rounding) event.pageY === event.clientY + window.scrollY; ``` Use `pageX/pageY` when you are placing something into the document's coordinate space — for example an absolutely positioned tooltip appended to `document.body` whose offset parent scrolls with the page. ## offsetX / offsetY — origin: the target's padding edge Measured from the padding edge of `event.target`. This is the seductive one: it *looks* like "position inside my element", and it is — but only when `event.target` is the element you had in mind. Because the DOM sets `target` to the deepest element under the pointer, a click on a `<span>` label inside a button reports `offsetX` relative to that span. With one delegated listener on a container, `offsetX` is relative to whatever child was hit, and it varies row by row. That mismatch is the usual reason a drag or a hit-test "jumps". ## The reliable element-local recipe ```js function localPoint(el, event) { const rect = el.getBoundingClientRect(); return { x: event.clientX - rect.left, y: event.clientY - rect.top }; } ``` Both operands are viewport-relative, so the subtraction is scroll-independent, and it works no matter which descendant was the actual target. `getBoundingClientRect()` returns the *transformed* border box, so a rotated or translated element still yields a sensible offset for translation and scaling. One caveat: if the element is CSS-scaled, the rect is in rendered pixels while the element's own layout coordinate system is not, so divide by the scale factor (`rect.width / el.offsetWidth`) when you need untransformed local units. Avoid the older `offsetLeft`-summing loops. They walk `offsetParent` chains, ignore transforms, and break inside scrolling containers. ## Related properties worth knowing - `movementX` / `movementY` — the delta from the previous mouse event, useful for pointer-lock-style relative input where absolute position is meaningless. - Touch events do not carry `offsetX`: each `Touch` object in `event.touches` exposes `clientX`, `pageX` and `screenX` only, which is another reason to derive positions from `clientX` plus a rect. - `MouseEvent` coordinates are in CSS pixels, so they already account for the device pixel ratio; you do not multiply by `devicePixelRatio` unless you are mapping into a canvas backing store. ## How this shows up in interviews The question is rarely "define pageX". It is a bug story: a drag works at the top of the page and drifts once the user scrolls (someone mixed `pageX` with a viewport-based rect), or a hit-test misfires only on rows that contain an icon (someone used `offsetX` under delegation). Naming the origin of each property answers both instantly.

  • Why does a drag implemented with pageX drift once the user scrolls, if the maths looked right?
    Almost always because it is mixed with a viewport-based measurement. `getBoundingClientRect()` returns viewport coordinates, so subtracting it from `pageX` leaves the scroll offset baked into the result, and the error grows exactly as far as the page is scrolled. Keep both sides in one space: `clientX` with the rect, or `pageX` with `rect.left + window.scrollX`.
  • When would offsetX actually be the right property to read?
    When the listener is attached directly to the element you care about and that element has no children that can become the target — a bare `<canvas>` is the classic case. Even then, many people still prefer the rect-based form so the code survives someone later nesting an element inside.
  • Do these coordinates change when the user zooms the browser?
    They are reported in CSS pixels, so under full-page zoom the numbers stay consistent with your layout rather than with device pixels. Pinch-zoom is different: it moves the visual viewport, so `clientX` is relative to the layout viewport and you need `window.visualViewport` offsets to reason about what is physically on screen.

saying these in an interview costs you the question

  • Says clientX includes the page scroll offset
  • Thinks offsetX is always relative to the listener's element
  • Sums offsetLeft up the offsetParent chain instead of using a rect
  • Mixes pageX with getBoundingClientRect values in one calculation
  • Claims screenX is the position inside the page

context