jQuery / Method / .offset() - Read or Set Document Coordinates
jQuery's .offset() returns { top, left } for the first selected element's border box relative to the document's top-left. .offset({ top, left }) moves selected elements to those document coordinates. Do not confuse these with viewport coordinates or parent-relative positions.
Distinguish offset, position and viewport coordinates
The .offset() API uses the document and measures the border box, excluding the target's margin. top and left are numbers in CSS pixels and may be fractional. Reading them does not move the element.
| Method | Reference frame | Target boundary |
|---|---|---|
| .offset() | Document | Border box |
| .position() | Offset parent's padding box | Target's margin box |
| getBoundingClientRect() | Viewport | Rectangle including borders |
The offset parent used by .position() is not necessarily the immediate HTML parent. getBoundingClientRect() gives top/left that change with scrolling; adding current window.scrollY/scrollX converts them to ordinary document coordinates. Do not copy values from different frames directly into CSS top/left.
Complete comparison of three frames and scrolling
Save all the code as UTF-8 in offset-demo.html and open it in a browser. Internet is required for the official jQuery file 4.0.0 from its content delivery network (CDN); no installation is needed. Reading, scrolling, moving, hiding, reset and the callback setter below were executed with 3.7.1 and 4.0.0.
stage sits at document top 500/left 20 with a 5-pixel border. target uses top 40/left 50 and margin 10 inside it. Body margin is zero and there are no transforms, allowing direct calculation. Controls stay fixed at the viewport top.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Document and viewport coordinates</title>
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
<style>
body { margin: 0; min-height: 1800px; font: 16px/1.6 sans-serif; }
#controls { position: fixed; inset: 0 0 auto; padding: 12px; background: #fff; z-index: 10; max-height: 42vh; overflow: auto; }
button { font: inherit; margin: 3px; }
pre { white-space: pre-wrap; overflow-wrap: anywhere; }
#stage { position: absolute; top: 500px; left: 20px; width: 300px; height: 260px; border: 5px solid #bbc; }
#target { position: absolute; top: 40px; left: 50px; margin: 10px; border: 2px solid teal; padding: 8px; background: #d9f4ef; }
</style>
</head>
<body>
<div id="controls">
<strong>Measure the target border box</strong>
<button type="button" id="read">Read</button>
<button type="button" id="scroll">Scroll 200px</button>
<button type="button" id="move">Move to (top 620, left 130)</button>
<button type="button" id="visibility">Toggle visibility</button>
<button type="button" id="display">Toggle display</button>
<button type="button" id="reset">Reset</button>
<pre id="result" aria-live="polite"></pre>
</div>
<div id="stage"><div id="target">Target</div></div>
<script>
$(function () {
const $target = $("#target");
function n(value) { return Number(value.toFixed(2)); }
function report() {
const node = $target.get(0);
if (!node) { $("#result").text("Target missing"); return; }
if (!node.getClientRects().length) { $("#result").text("Not rendered; reveal before measuring"); return; }
const offset = $target.offset();
const rect = node.getBoundingClientRect();
const position = $target.position();
$("#result").text(`document top=${n(offset.top)}, left=${n(offset.left)}\nviewport top=${n(rect.top)}, left=${n(rect.left)}\nposition top=${n(position.top)}, left=${n(position.left)}\nscrollY=${n(window.scrollY)}`);
}
$("#read").on("click", report);
$("#scroll").on("click", function () { window.scrollTo(0, 200); });
$("#move").on("click", function () { if ($target.get(0).getClientRects().length) $target.offset({ top: 620, left: 130 }); report(); });
$("#visibility").on("click", function () { $target.css("visibility", $target.css("visibility") === "hidden" ? "visible" : "hidden"); report(); });
$("#display").on("click", function () { $target.toggle(); report(); });
$("#reset").on("click", function () { $target.css({ top: 40, left: 50, display: "", visibility: "" }); window.scrollTo(0, 0); report(); });
$(window).on("scroll.offsetLesson resize.offsetLesson", report);
report();
});
</script>
</body>
</html>
Initial output at scroll zero follows. Copying only a fragment into another page can change the surrounding layout and these numbers.
document top=555, left=85
viewport top=555, left=85
position top=40, left=50
scrollY=0
- Read measures again. Document top is 500 + 5 + 40 + 10 = 555; left is 20 + 5 + 50 + 10 = 85.
- Scroll 200px keeps document top 555 but gives viewport top 355 and scrollY 200. .position() stays top 40/left 50.
- Move to (top 620, left 130) sets document 620/130. At scroll 200, viewport is 420/130 and .position() 105/95, showing that parent CSS positions differ from document coordinates.
- Toggle visibility hides the target while retaining its layout box, so it remains measurable.
- Toggle display makes display:none. The example shows Not rendered instead of unsupported measurements. Press again to measure it.
- Reset restores CSS position and visibility and returns scrolling to zero.
The target here is an ordinary absolutely positioned element. Window scrolling alone leaves document coordinates unchanged when its position stays fixed in the document. Do not generalize this to fixed/sticky positioning or layout changes. Remeasure when needed after images, fonts or viewport size change.
Set coordinates and use the callback form
.offset({ top: 620, left: 130 }) accepts document coordinates, not parent-relative CSS top/left. jQuery calculates the required positioning values and may change static positioning to relative for a setter. Check the resulting CSS and layout too.
.offset(function(index, current)) receives each element's current document coordinates and returns new top/left. Run this complete fragment in the developer tools Console. It creates two temporary elements, applies different vertical increments and cleans up at the end.
const $items = $("<div>A</div><div>B</div>")
.css({ position: "absolute", top: 1000, left: 20 }).appendTo("body");
$items.offset(function (index, current) {
return { top: current.top + index * 30, left: current.left + 40 };
});
console.log($items.map(function () {
const point = $(this).offset();
return `${point.top},${point.left}`;
}).get().join(" | "));
console.log(`empty getter=${typeof $().offset()}`);
$items.remove();
1000,60 | 1030,60
empty getter=undefined
The getter reads only the first element; setters affect every selected element. index is zero-based within that collection. The demo rounds only its displayed output for readability, not the geometry used in calculations. Do not discard fractional coordinates automatically.
Check hidden elements, missing targets and layout changes
display:none has no rendering box, so it is not a supported source of meaningful current screen coordinates. Even if a runtime returns values such as zero, reveal it before measuring instead of accepting that as its position. visibility:hidden retains a box and can be measured.
The example checks node existence and getClientRects().length first. An empty jQuery selection's .offset() is undefined; reading .offset().top directly would fail. Do not assume a node detached from the Document Object Model (DOM) provides the intended document position either.
Inspect browser zoom, transforms, changed margins/borders and ancestor layout. The official API does not support accounting for margins on the html element. Align coordinate frames first; update at relevant layout changes rather than repeating geometry reads and CSS writes unnecessarily.




