skip to content

In the DOM, why does reading `el.style.width` often return an empty string while `el.offsetWidth` returns a number, and which of the two can make the browser do layout work?

level: juniorimportance: should knowfreq 45%

answer

  1. two different notions of width
  2. inline attribute versus rendered box
  3. style reads back what you set
  4. offsetWidth counts padding and border
  5. rendered numbers can cost layout

basics

~20 s

el.style.width reads only the element's inline style attribute, so it is empty unless something set it inline. el.offsetWidth reports the element's actual rendered border-box width, so the browser may have to compute pending layout before it can answer.

solid answer

~40 s

They answer two different questions. `el.style` is a `CSSStyleDeclaration` over the element's **inline** `style` attribute only — if the width came from a stylesheet rule, `el.style.width` is `""`, and it never tells you what the element actually looks like. `el.offsetWidth` is a layout-derived number: the rendered border-box width, including padding and border, excluding margin, rounded to an integer. Because it has to reflect reality, the browser must have up-to-date layout to answer it, and will recompute pending style and layout synchronously if anything changed since the last update. So the inline read is a cheap attribute lookup; the geometry read can be one of the most expensive lines in a frame. `getComputedStyle(el).width` sits between them: it gives the resolved pixel width from any source, and it too needs current layout.

code

javascript · 11 lines
javascript
const style = document.createElement('style');
style.textContent = '.box { width: 100px; padding: 10px; border: 2px solid; }';
document.head.append(style);

const box = document.createElement('div');
box.className = 'box';
document.body.append(box);

console.log(box.style.width);              // "" - nothing was set inline
console.log(getComputedStyle(box).width);  // "100px" - used content width
console.log(box.offsetWidth);              // 124 - content + padding + border

go deeper

for a junior

Be ready to say that el.style exposes only the inline style attribute, and that offsetWidth is the rendered border-box width returned as an integer.

for a middle

Explain all three ladders — the inline declaration, getComputedStyle's resolved value, and the layout-derived offset and rect numbers — and say which of them need up-to-date layout.

for a senior

Point out that the layout-derived reads are the ones that can stall a frame, and describe where in a component's update cycle you would take a measurement so it costs a single flush.

for a principal

Own the guidance: agree a house rule for where in the component lifecycle measurement is allowed to happen, so measurement cost stays predictable as the number of components grows.

## Three different "widths" An element can tell you its width in at least three ways, and they are not interchangeable. 1. **The inline declaration** — `el.style.width`. This is the `style` **attribute** on that one element, exposed as a `CSSStyleDeclaration`. It is a plain string you can read and write. 2. **The resolved value** — `getComputedStyle(el).width`. This is what the cascade produced for this element after stylesheets, inheritance and the default styles, expressed in absolute units. 3. **The rendered geometry** — `el.offsetWidth`, `el.clientWidth`, `el.getBoundingClientRect().width`. These are numbers taken from the box the browser actually laid out on the page. ## Why `style.width` is usually empty `el.style` is not a view of the cascade. It is a view of the inline attribute. If the width comes from a stylesheet rule, a `<link>`ed file, or nothing at all, there is no inline declaration to report and the property is the empty string. It becomes non-empty only when markup wrote `style="width: …"` or JavaScript assigned `el.style.width = …`. This catches people out in both directions: reading it to "find out how wide the element is" returns nothing useful, and writing to it silently adds an inline declaration that outranks most stylesheet rules. ## What `offsetWidth` measures `offsetWidth` is the **border-box** width of the element's rendered box: content plus padding plus border, excluding margin. It is an integer, rounded from the real fractional layout value. For an element that is not rendered at all — `display: none`, or detached from the document — it is `0`, because there is no box. ```js // .box { width: 100px; padding: 10px; border: 2px solid } box.style.width; // "" nothing was set inline getComputedStyle(box).width; // "100px" the used content width box.offsetWidth; // 124 100 + 2*10 + 2*2 ``` `getBoundingClientRect()` is the more precise sibling: it returns a `DOMRect` with fractional values, positioned relative to the viewport, and it reflects CSS transforms applied to the element. `offsetWidth` is the untransformed layout width, rounded. ## Which one costs the browser something Reading the inline attribute is a string lookup — effectively free. The layout-derived numbers are not, because the value they describe may not exist yet. The browser batches DOM and style changes and normally recomputes layout once, right before it paints a frame. A geometry read is defined to return a current value, so if anything is still pending the browser must recalculate style and run layout **immediately**, in the middle of your script, before the read returns. That is a forced synchronous layout. `getComputedStyle(el).width` is in the same category: to give you a pixel value for `width` it needs layout to have run. Asking it for a property that layout does not affect, such as `color`, only needs style to be current. ## Practical rules - To find out what you set inline, read `el.style.x`. To find out what the element actually is, read geometry or computed style. - Never assume `el.style.width` round-trips a stylesheet value; it does not. - Remember `offsetWidth` includes padding and border while `clientWidth` includes padding but not border, and `scrollWidth` describes the content including what overflows. - Treat geometry reads as calls that can do work, not as fields. One read is fine; a read taken repeatedly in a loop that also writes styles is the expensive pattern.

  • What does offsetWidth report for an element with display: none?
    Zero. `display: none` means the element generates no box at all, so there is no rendered geometry to report, and every offset and client measurement comes back as `0`. The same is true for an element that is not attached to the document. This is why code that measures a hidden element before revealing it gets zeros and has to measure after it is rendered.
  • How does getBoundingClientRect().width differ from offsetWidth?
    `getBoundingClientRect()` returns fractional values, is measured relative to the viewport, and reflects CSS transforms — a scaled element reports its scaled size. `offsetWidth` is the untransformed layout width rounded to an integer, relative to nothing in particular. Use the rect when you need sub-pixel precision or on-screen position; either one can force the browser to compute pending layout.

saying these in an interview costs you the question

  • Thinks el.style reflects stylesheet rules, not just inline ones
  • Says offsetWidth excludes padding and border
  • Treats offsetWidth as free because it looks like a field
  • Uses clientWidth and offsetWidth interchangeably
  • Expects style.width to return a number rather than a string

context