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.

margin (not part of the element)
border
padding
content
scrollbar
The box model from outside in. Geometry properties measure different combinations of these regions.

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:

border 15pxpadding 25pxcontent 280 x 220scrollbar
The sample element rendered, with its border, padding, content, and scrollbar 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:

offsetWidth = 360clientWidth = scrollWidth = 314offsetHeight = 300clientHeight = 270
Every property at once. Widths run across the top and bottom, heights down the sides; scrollWidth equals clientWidth here, and scrollHeight (680) also counts the content hidden below the fold.

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.

propertyborderpaddingcontentscrolled-outour value
offsetWidth/Height360 / 300
clientWidth/Height● (no bar)314 / 270
scrollWidth/Height314 / 680
clientTop/Leftoffset from outer to inner corner (usually border width)15 / 15
What each property measures. Regions included are marked with a dot.

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:

interactiveRead every geometry property live

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:

  1. CSS-positioned — its position is absolute, relative, fixed, or sticky, or
  2. a <td>, <th>, or <table>, or
  3. 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.

main (position: relative)article (static, skipped)cardoffsetLeft = 200offsetTop = 140
The card is positioned absolutely, so offsetLeft/offsetTop are measured from the corner of its offsetParent — the positioned <main>, not the skipped <article>.

offsetParent is null in a few situations:

  1. The element (or an ancestor) is hidden with display:none, or it isn’t in the document at all.
  2. The element is <body> or <html> itself.
  3. The element has position:fixed.

offsetWidth/offsetHeight

Now the element itself. These two are the easy ones: the full outer size, borders included.

offsetWidth = 360offsetHeight = 300
offsetWidth and offsetHeight span the full outer box, 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.
15252802515= 360
offsetWidth adds up the whole box across its width.

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.
bordercontent + paddingclientLeft = 15clientTop = 15
Zoomed top-left corner: clientLeft and clientTop are the gap from the outer corner to the inner (content + padding) corner — here, just the border widths.

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.

borderscrollbarcontent + paddingclientLeft = 31clientTop = 15
Under right-to-left layout the scrollbar moves to the left, so clientLeft now spans the border plus the scrollbar: 15 + 16 = 31.

clientWidth/clientHeight

These give the size of the visible area inside the borders: content plus padding, but excluding the scrollbar.

clientWidth = 314clientHeight = 270
clientWidth and clientHeight cover content plus padding inside the borders, but stop short of 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:

content only (padding: 0)clientWidthclientHeight
With padding: 0, clientWidth and clientHeight collapse to the pure content area inside the borders, still 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.

scrolled-out content(hidden below the fold)scrollWidth = 314scrollHeight = 680
scrollHeight measures the full content including the part scrolled below the fold; scrollWidth equals clientWidth because nothing overflows sideways.
  • 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 matches clientWidth.
visible
clientHeight
scrolled out
(hidden)
clientHeight = visible only
scrollHeight = visible + hidden = 680
clientHeight is the visible window; scrollHeight is the whole content, hidden part included.

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:

interactiveExpand a box to its full content height

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.”

scrolled upvisible windowclientHeightbelow (hidden)scrollTopscrollHeight
scrollTop is the height of the content that has been scrolled up out of view; the visible window sits below it, and more content may wait beneath.

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).

scrollTop
clientHeight
(visible)
below
scrollHeight = whole stripscrollTop = 60 → scrolled down 60px
scrollTop is the hidden slice above the viewport; the three add up to the full content.

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:

interactiveWrite to scrollTop to move the content

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:

interactiveclientWidth vs getComputedStyle().width

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 a td/th/table/body.
  • offsetLeft/offsetTop — coordinates relative to offsetParent’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, so clientLeft also picks up its width.
  • clientWidth/clientHeight — content plus padding, scrollbar excluded.
  • scrollWidth/scrollHeight — same as client*, 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.