getBoundingClientRect in JavaScript: DOMRect, viewport coordinates, and performance

Tech reviewed: Deepak Prasad
getBoundingClientRect in JavaScript: DOMRect, viewport coordinates, and performance

Developers look this API up under many names: getboundingclientrect as one token, get bounding client rect with spaces, getboundingclientrect javascript, javascript getboundingclientrect, js getboundingclientrect, or simply boundingclientrect. Two themes show up constantly in search: what DOMRect.width actually measures (the same intent as mdn getboundingclientrect width includes padding border), and getboundingclientrect performance when the method is called inside scroll or animation loops.

Element.getBoundingClientRect() takes no arguments. It returns a DOMRect (or an object that quacks like one) in CSS pixels relative to the viewport, with x / y aligned to left / top. Margins are outside the box the rectangle describes; padding and border are inside the width / height border box, matching the geometry described on MDN and in the CSSOM View Module.

Pick the element first (get element in JavaScript, DOM selectors); for width-only reads that do not need viewport position, compare offsetWidth / clientWidth.

The HTML below is the same compact fixture used to capture the printed geometry.

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
#probe {
  width: 40px;
  height: 12px;
  padding: 5px;
  border: 2px solid black;
  margin: 10px;
  display: block;
  box-sizing: content-box;
}
#gone {
  display: none;
  width: 100px;
  height: 50px;
}
#invis {
  visibility: hidden;
  width: 10px;
  height: 10px;
  display: block;
  box-sizing: content-box;
}
</style>
</head>
<body>
<div id="probe"></div>
<div id="gone"></div>
<div id="invis"></div>
</body>
</html>

The printed numbers below were captured with jsdom 24.0.0 plus the same border-box layout shim (jsdom otherwise leaves layout geometry at zero). In a desktop browser on this markup, width and height match the same border-box totals; left / top / x / y here are all 0 because the fixture sits at the origin of the jsdom viewport.

Tested on: Node.js v20.18.2 with jsdom for geometry snippets. {run=false} on DOM fences matches hosts without a layout engine.


DOMRect.width and DOMRect.height (border box)

For a rendered element, rect.width and rect.height from getBoundingClientRect() describe the element’s border box after layout, including horizontal and vertical padding and border on that axis, in CSS pixels. They do not add the margin. That answers the classic “mdn getboundingclientrect width includes padding border” wording: yes—the border-box width and height include padding and border (and inline-axis scrollbars when they consume space), but not margin.

For #probe, the horizontal border box is 40 + (5 + 5) padding + (2 + 2) border → 54 px; the vertical border box is 12 + (5 + 5) + (2 + 2)26 px.

javascript
const rect = document.getElementById("probe").getBoundingClientRect();
console.log(rect.width);
console.log(rect.height);

You should see 2 lines, in order: 54, 26.


left, top, x, and y

left and top are distances from the viewport’s top-left corner to the element’s border box. x and y are aliases for left and top.

javascript
const rect = document.getElementById("probe").getBoundingClientRect();
console.log(rect.left);
console.log(rect.top);
console.log(rect.x);
console.log(rect.y);

You should see 4 lines, in order: 0, 0, 0, 0.

If the element is scrolled above the viewport, top can be negative; if it sits to the left, left can be negative—even though width / height stay non-zero when the box exists.


Document coordinates with scrollX / scrollY

getBoundingClientRect() is viewport-relative. To approximate position in document coordinates, add the window scroll offset:

javascript
const el = document.getElementById("probe");
const rect = el.getBoundingClientRect();
console.log(rect.top + window.scrollY);
console.log(rect.left + window.scrollX);

You should see 2 lines, in order: 0, 0.

In this harness scrollY / scrollX are zero; after scrolling a long page in a browser, the sums move with the document.


display: none vs visibility: hidden

With display: none, the element does not participate in layout, so every DOMRect dimension is zero:

javascript
const rect = document.getElementById("gone").getBoundingClientRect();
console.log(rect.width);
console.log(rect.height);

You should see 2 lines, in order: 0, 0.

visibility: hidden still reserves layout space, so the border box size is preserved:

javascript
const rect = document.getElementById("invis").getBoundingClientRect();
console.log(rect.width);
console.log(rect.height);

You should see 2 lines, in order: 10, 10.


getBoundingClientRect performance and layout

Reading geometry after a DOM or style change can force the browser to flush pending style and layout work so the numbers are up to date. Alternating writes and reads in a tight loop (for example, setting element.style.width then immediately calling getBoundingClientRect() on many siblings) causes layout thrashing and is a common source of jank.

Mitigations:

  • Batch writes (change classes or cssText once), then batch reads, ideally inside requestAnimationFrame when tying work to paint.
  • Prefer IntersectionObserver for “is this near the viewport?” instead of calling getBoundingClientRect() on every scroll event.
  • Prefer ResizeObserver when you only care about size changes, not absolute viewport coordinates.

Transforms and zoom change how CSS pixels map to the screen; getBoundingClientRect() reflects the transformed border box in those CSS pixels—avoid hand-dividing by guessed scale factors unless you really control the math.


SVG and other roots

You can call getBoundingClientRect() on SVGElement instances; the rectangle is the tight axis-aligned box in viewport space, still subject to transforms on the node or its ancestors.


Summary

getBoundingClientRect javascript lookups are really about Element.getBoundingClientRect(): a DOMRect in CSS pixels relative to the viewport, including padding and border but not margin. People ask whether width “includes padding and border” like the MDN phrasing—yes, width / height describe the border box after layout, while margin sits outside.

Performance FAQs focus on calling the API inside tight scroll handlers: each read can force layout, so batch DOM writes before reads, prefer IntersectionObserver for visibility, and use ResizeObserver when you only care about size changes. Remember transforms and zoom map into the returned rectangle, so avoid mixing hand-tuned scale math unless you control the entire rendering pipeline.


References

Olorunfemi Akinlua

Boasting over five years of experience in JavaScript, specializing in technical content writing and UX design. With a keen focus on programming languages, he crafts compelling content and designs user-friendly interfaces to enhance digital …

  • JavaScript
  • Web Design