getBoundingClientRect
Viewport-relative boxes with getBoundingClientRect — subpixels, scroll, transforms, and layout thrashing pitfalls.
- 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.
Related rect APIs
| 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.”
Related
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
- client, offset, and scroll DimensionsclientWidth vs offsetWidth vs scrollWidth — borders, scrollbars, and which box measurement to use for layout math.
- classList and datasetToggle CSS classes with classList and read data-* attributes via dataset — tokens, naming, and DOM performance notes.
- contentEditable PitfallsWhy contentEditable is hard: browser HTML mess, carets, sanitization, undo, and when to pick a real editor framework.
- Creating and Updating DOM NodescreateElement, textContent vs innerHTML, insert APIs, and batching updates — safe DOM writes without accidental XSS.
- Custom EventsCustomEvent, detail payloads, bubbles and composed — decoupling components without a global event bus mess.