Element size and scrolling
Every element on a page occupies a rectangle. The DOM exposes a family of read-only properties that tell you exactly how big that rectangle is, where its borders sit, how much padding wraps the content, and how far the content has scrolled. All of it in pixels, all of it as plain numbers you can do math with.
You reach for these constantly: positioning a tooltip next to a button, measuring whether a menu fits on screen, syncing a scroll indicator, animating an accordion open to its natural height. Once you know which property measures which part of the box, the rest is arithmetic.
The mental model: nested boxes
Before any property names, hold this picture in your head. An element is a set of nested rectangles. From the outside in: the border, then the padding, then the content. If the content overflows and overflow allows scrolling, a scrollbar eats into that space too. Margins sit outside the border and are not considered part of the element — there are no geometry properties for them.
Sample element
Every measurement below refers to one concrete element. Pin it down now so the numbers make sense later:
<div id="card">
...Text...
</div>
<style>
#card {
width: 280px;
height: 220px;
border: 15px solid #C99A5B;
padding: 25px;
overflow: auto;
}
</style>
This one has it all: a thick border, padding on every side, and overflow: auto so a scrollbar appears when the text is too tall. No margins, because margins live outside the element and have no dedicated geometry property.
Rendered, with all four regions labeled:
You can open the document in the sandbox to poke at it live.
Geometry at a glance
Here is the full map of properties overlaid on the element:
The values are numbers, but every one of them is a count of CSS pixels — treat them as pixel measurements. Crucially they are plain numbers (360), never strings with units ("360px"), so you can add, subtract, and compare them directly.
This table is the whole chapter in miniature. Skim it now, then read the sections for the why behind each row.
| property | border | padding | content | scrolled-out | our value |
|---|---|---|---|---|---|
| offsetWidth/Height | ● | ● | ● | — | 360 / 300 |
| clientWidth/Height | — | ● | ● (no bar) | — | 314 / 270 |
| scrollWidth/Height | — | ● | ● | ● | 314 / 680 |
| clientTop/Left | offset from outer to inner corner (usually border width) | 15 / 15 | |||
We’ll walk the properties from the outside of the element inward. But first, see all of them at once on a live element. Add or remove text and watch which numbers move — only scrollHeight grows past the visible window, because it alone counts the hidden overflow:
offsetParent, offsetLeft/offsetTop
You will rarely touch these, but they sit at the outermost layer, so they come first.
offsetParent points to the nearest ancestor the browser uses as the reference frame when it lays this element out. That ancestor is the closest one that is any of:
- CSS-positioned — its
positionisabsolute,relative,fixed, orsticky, or - a
<td>,<th>, or<table>, or - the
<body>.
Given that anchor, offsetLeft and offsetTop are the x/y coordinates of the element relative to the offsetParent’s top-left corner.
<main style="position: relative" id="main">
<article>
<div id="card" style="position: absolute; left: 200px; top: 140px">...</div>
</article>
</main>
<script>
alert(card.offsetParent.id); // main
alert(card.offsetLeft); // 200 (a number, not the string "200px")
alert(card.offsetTop); // 140
</script>
Notice that <article> is skipped — it isn’t positioned and isn’t a table cell, so the browser walks past it up to <main>, which is position: relative.
offsetParent is null in a few situations:
- The element (or an ancestor) is hidden with
display:none, or it isn’t in the document at all. - The element is
<body>or<html>itself. - The element has
position:fixed.
offsetWidth/offsetHeight
Now the element itself. These two are the easy ones: the full outer size, borders included.
For the sample element:
offsetWidth = 360— content CSS-width (280) plus both paddings (2 × 25) plus both borders (2 × 15).offsetHeight = 300— the same idea vertically:220 + 2×25 + 2×15.
clientTop/clientLeft
Step inside the outer edge and you hit the borders. clientTop and clientLeft measure your way across them.
For the sample:
clientLeft = 15— the left border width.clientTop = 15— the top border width.
The precise definition is subtler than “border width.” These properties are the coordinates of the inner (content + padding) corner relative to the outer corner. In a normal left-to-right layout that distance equals the left/top border, so the distinction never surfaces.
It surfaces under right-to-left text — an OS set to Arabic or Hebrew, for instance. There the vertical scrollbar moves to the left side of the element, so the space between the outer-left edge and the inner-left edge now includes both the border and the scrollbar. clientLeft becomes 15 + 16 = 31, not 15.
clientWidth/clientHeight
These give the size of the visible area inside the borders: content plus padding, but excluding the scrollbar.
Take clientHeight first. There’s no horizontal scrollbar, so it’s simply everything between the top and bottom borders: CSS-height 220 plus both vertical paddings (2 × 25), giving 270.
clientWidth is where the scrollbar bites. The content isn’t 280 wide here — the 16px scrollbar has claimed part of it, leaving 264. Add the two horizontal paddings (2 × 25) and you get 314.
Strip the padding and clientWidth/clientHeight become the pure content area, inside the borders and excluding the scrollbar:
That makes them the go-to properties for “how much room does the content actually have” — as long as you account for padding when it exists.
scrollWidth/scrollHeight
Same idea as clientWidth/clientHeight, but these count the parts scrolled out of view too. They measure the full content, including what’s hidden above, below, or beside the visible window.
scrollHeight = 680— the entire inner height of the content, hidden overflow included.scrollWidth = 314— the full inner width. There’s no horizontal overflow here, so it matchesclientWidth.
clientHeight
(hidden)
One practical use: grow a box to fit all its content, killing the scrollbar.
// expand the element to its full content height
element.style.height = `${element.scrollHeight}px`;
That single line is the backbone of auto-expanding textareas and “read more” panels. Read the natural content height, then set the box to exactly that. In HTML the technique looks like this — a button that snaps the box open:
<div id="element" style="width:300px;height:200px;padding:0;overflow:auto;border:1px solid black">
text text text ... (a lot of it, enough to overflow)
</div>
<button onclick="element.style.height = `${element.scrollHeight}px`">
element.style.height = `${element.scrollHeight}px`
</button>
Here it is running. The box starts clipped with a scrollbar; one click reads scrollHeight and expands the box to exactly that, so the scrollbar vanishes:
scrollLeft/scrollTop
scrollLeft and scrollTop report how far the content has been scrolled — the size of the hidden portion that has been pushed off the top-left. Read scrollTop as “how much has scrolled up out of view.”
The three vertical measurements fit together cleanly: at any moment, scrollTop + clientHeight is how far down the content you can currently see, and that can range from clientHeight (scrolled to the top) up to scrollHeight (scrolled to the bottom).
Try both moves below. Scroll the box by hand and the live scrollTop readout follows; the buttons then write to scrollTop to jump straight to either end:
Don’t read width/height from CSS
You’ve now got DOM geometry properties for widths, heights, and distances. But from the Styles and classes chapter you also know getComputedStyle can hand you the CSS width:
let elem = document.body;
alert( getComputedStyle(elem).width ); // the CSS width of elem
So why bother with geometry properties? Three reasons, and each is a real bug waiting to happen.
1. CSS width depends on box-sizing. That property redefines what “width” even means — content-box versus border-box. Flip box-sizing for a purely visual reason and any JavaScript that trusted getComputedStyle(...).width silently starts computing the wrong number.
2. CSS width can be auto. For an inline element it usually is:
<span id="elem">Hello!</span>
<script>
alert( getComputedStyle(elem).width ); // auto
</script>
auto is perfectly valid CSS, but useless for arithmetic. You wanted a pixel count to feed into a calculation and got a keyword instead.
3. The scrollbar isn’t handled consistently. Code that behaves without a scrollbar can break once one appears, because the scrollbar steals width from the content. clientWidth/clientHeight already subtract it. getComputedStyle(...).width does not agree across browsers: some (like Chrome) return the real inner width minus the scrollbar, while others (like Firefox) return the full CSS width and ignore the scrollbar. That split alone is enough to avoid it.
If your browser reserves space for the scrollbar — most desktop Windows browsers do — you can watch the discrepancy live:
The element has CSS width:260px. On desktop Windows, Firefox, Chrome, and Edge all reserve scrollbar space, yet Firefox reports 300px while Chrome and Edge report less. Firefox hands back the declared CSS width; the others hand back the real content width. And this gap is purely a JavaScript-reading quirk — visually the page renders identically in all of them.
Summary
Elements carry this set of geometry properties:
offsetParent— the nearest positioned ancestor, or atd/th/table/body.offsetLeft/offsetTop— coordinates relative tooffsetParent’s top-left corner.offsetWidth/offsetHeight— the full outer size, borders included.clientLeft/clientTop— the distance from the outer top-left corner to the inner (content + padding) top-left corner. In left-to-right layouts that’s just the left/top border widths; in right-to-left layouts the vertical scrollbar sits on the left, soclientLeftalso picks up its width.clientWidth/clientHeight— content plus padding, scrollbar excluded.scrollWidth/scrollHeight— same asclient*, but counting the scrolled-out, invisible content too.scrollLeft/scrollTop— how far the content is scrolled from its top-left corner.
Everything here is read-only except scrollLeft/scrollTop, which scroll the element when you assign to them.