client, offset, and scroll Dimensions
clientWidth vs offsetWidth vs scrollWidth — borders, scrollbars, and which box measurement to use for layout math.
- javascript
- dom
- layout
- scroll
Layout interviews and sticky-header bugs all ask the same thing: which width? The DOM exposes several integer measurements that include different pieces of the CSS box. Mixing them is how you get 15px off forever.
The family
For an element el:
| Property | Roughly includes |
|---|---|
clientWidth / clientHeight |
content + padding − scrollbar |
offsetWidth / offsetHeight |
content + padding + border + scrollbar |
scrollWidth / scrollHeight |
full content size including overflow |
clientTop / clientLeft |
border widths (top/left) |
offsetTop / offsetLeft |
distance to offsetParent |
const el = document.querySelector('.panel');
el.clientWidth; // visible inner width
el.offsetWidth; // layout width including border
el.scrollWidth; // content width if it didn't clip
// overflow detection
const canScrollX = el.scrollWidth > el.clientWidth;
const canScrollY = el.scrollHeight > el.clientHeight;
Mental picture
Imagine a box with padding, border, and a vertical scrollbar:
- clientWidth — CSS width of content box + padding, but not the border; scrollbar space is excluded from the remaining client area.
- offsetWidth — full layout width: border box as laid out (includes scrollbar gutter in typical engines).
- scrollWidth — how wide the content is, even the part hidden by overflow.
function overflowAmounts(el) {
return {
x: el.scrollWidth - el.clientWidth,
y: el.scrollHeight - el.clientHeight,
};
}
offsetParent and coordinates
// position relative to offsetParent (not always document)
const { offsetTop, offsetLeft, offsetParent } = el;
// walk to document origin (simple version; transforms break this)
function pageOffset(node) {
let x = 0;
let y = 0;
while (node) {
x += node.offsetLeft;
y += node.offsetTop;
node = node.offsetParent;
}
return { x, y };
}
For viewport-relative boxes, prefer getBoundingClientRect() — it understands transforms. offset* ignores CSS transforms.
Scroll position
el.scrollTop; // how far scrolled down
el.scrollLeft;
// document scroll (quirks across roots)
const y =
window.scrollY ??
document.documentElement.scrollTop;
Stick to window.scrollX / scrollY for the viewport when you can.
Reading forces layout
These getters flush pending style/layout. In a loop, they cause layout thrashing:
// bad: read/write interleave
items.forEach((item) => {
const h = item.offsetHeight; // read
item.style.height = h + 10 + 'px'; // write
});
// better: batch reads, then writes
const heights = items.map((i) => i.offsetHeight);
items.forEach((item, i) => {
item.style.height = heights[i] + 10 + 'px';
});
Practical recipes
// sticky footer: is user near bottom?
function nearBottom(el, px = 80) {
return el.scrollTop + el.clientHeight >= el.scrollHeight - px;
}
// center a child in a scrollport
function scrollChildIntoCenter(parent, child) {
const top =
child.offsetTop - parent.clientHeight / 2 + child.offsetHeight / 2;
parent.scrollTo({ top, behavior: 'smooth' });
}
Interview answer
“clientWidth is content+padding without border/scrollbar; offsetWidth is the border-box layout width; scrollWidth is the full content width including overflow. I use scroll vs client to detect overflow, getBoundingClientRect for viewport boxes with transforms, and I batch reads to avoid layout thrashing.”
Related
Subpixel and scrollbar gutters
// classic “100vw includes scrollbar” layout bug — measure carefully
const layoutWidth = document.documentElement.clientWidth; // excludes scrollbar
const windowInner = window.innerWidth; // may include scrollbar gutter depending on platform
function scrollbarSize() {
return window.innerWidth - document.documentElement.clientWidth;
}
offsetParent is null for position: fixed elements and root cases — don’t walk blindly. For virtualized lists, store item heights from one measurement pass; re-measure only on resize (ResizeObserver) rather than reading offsetHeight during scroll.
When comparing “did size change?”, prefer ResizeObserver over polling clientWidth in a rAF loop.
Further reading
Related guides
- getBoundingClientRectViewport-relative boxes with getBoundingClientRect — subpixels, scroll, transforms, and layout thrashing pitfalls.
- 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.