ESC

Type to search the knowledge base.

getBoundingClientRect

Viewport-relative boxes with getBoundingClientRect — subpixels, scroll, transforms, and layout thrashing pitfalls.

intermediate3 min read
  • javascript
  • getboundingclientrect
  • layout
  • dom

element.getBoundingClientRect() returns a DOMRect for the element’s border box relative to the viewport. It respects CSS transforms, which is why tooltips, popovers, and collision checks prefer it over offsetTop walks.

What you get

const r = el.getBoundingClientRect();
// {
//   x, y,          // alias of left/top in modern browsers
//   width, height,
//   top, right, bottom, left
// }

Coordinates: top/left are distance from the viewport origin. Scroll the page and they change — they are not document coordinates.

// document-relative position
const docTop = r.top + window.scrollY;
const docLeft = r.left + window.scrollX;

Transforms and subpixels

el.style.transform = 'scale(2)';
const r = el.getBoundingClientRect();
// width/height reflect the transformed visual box

Values are doubles (fractional pixels). Rounding for canvas or integer styles is your job:

const { width, height } = el.getBoundingClientRect();
canvas.width = Math.round(width * devicePixelRatio);

Intersection / sticky UI

function isFullyVisible(el) {
  const r = el.getBoundingClientRect();
  return (
    r.top >= 0 &&
    r.left >= 0 &&
    r.bottom <= window.innerHeight &&
    r.right <= window.innerWidth
  );
}

function placeTooltip(anchor, tooltip) {
  const a = anchor.getBoundingClientRect();
  tooltip.style.position = 'fixed';
  tooltip.style.top = `${a.bottom + 8}px`;
  tooltip.style.left = `${a.left}px`;
}

For continuous visibility tracking, prefer IntersectionObserver — it avoids per-scroll layout reads.

Layout thrashing

getBoundingClientRect forces layout if style changes are pending.

// bad
items.forEach((item) => {
  item.style.width = 'auto';
  const w = item.getBoundingClientRect().width; // layout each time
  item.style.width = w + 10 + 'px';
});

// better: read all, then write all
const widths = items.map((i) => i.getBoundingClientRect().width);
items.forEach((item, i) => {
  item.style.width = widths[i] + 10 + 'px';
});

Batch with requestAnimationFrame when following animations.

API Notes
getClientRects() multiple rects for inline split lines
Range.getBoundingClientRect() text selection bounds
offsetWidth integers, no transforms
const lineRects = el.getClientRects(); // TextRectangleList

Hidden elements

display: none → typically zeros. visibility: hidden still has geometry. Offscreen with transform still has a rect — useful for measuring before reveal.

Interview answer

“getBoundingClientRect returns viewport-relative geometry including transforms and fractional pixels. I add scrollX/Y for document coordinates. It forces layout, so I batch reads. For scroll visibility I prefer IntersectionObserver; for tooltips and popovers I use the rect plus position fixed/absolute.”

Anchoring popovers with collision flips

function positionMenu(button, menu) {
  const br = button.getBoundingClientRect();
  const mr = menu.getBoundingClientRect();
  let top = br.bottom + 4;
  let left = br.left;
  if (top + mr.height > window.innerHeight) {
    top = br.top - mr.height - 4; // flip up
  }
  if (left + mr.width > window.innerWidth) {
    left = window.innerWidth - mr.width - 8;
  }
  menu.style.transform = `translate(${left}px, ${top}px)`;
}

Re-run on resize and scroll of any scrollport ancestors (not only window). Floating UI libraries exist because nested scroll + transforms get messy — use them once requirements grow past a single flip.

iframe and visual viewport

// visualViewport accounts for mobile chrome / pinch zoom better than innerHeight alone
const vv = window.visualViewport;
const visibleBottom = vv ? vv.height + vv.offsetTop : window.innerHeight;

Rects are relative to the layout viewport of the element’s document. Across iframes, compare coordinates only after mapping with frame offsets. For sticky headers, subtract header height from available space when flipping popovers — measure the header with its own getBoundingClientRect.

Further reading

Related guides