In a ResizeObserver callback, how do an entry's contentRect, contentBoxSize, borderBoxSize and devicePixelContentBoxSize differ, and what does the box option passed to observe() actually control?
answer
- four descriptions, one element
- logical axes, not width and height
- the option picks the trigger
- index [0], it is a list
basics
~20 scontentRect is the legacy content-box rectangle; contentBoxSize, borderBoxSize and devicePixelContentBoxSize are arrays of {inlineSize, blockSize} for the content box, the border box, and the content box in device pixels. The box option chooses which box's changes trigger a notification, not which properties you get.
solid answer
~40 sEvery `ResizeObserverEntry` carries all four descriptions regardless of how you observed. `contentRect` is the original API: a `DOMRectReadOnly` whose `width`/`height` are the content box, and whose `x`/`y` are the padding offsets, not viewport coordinates. `contentBoxSize` and `borderBoxSize` are the modern replacements — arrays of `ResizeObserverSize` with `inlineSize` and `blockSize`, which are writing-mode relative, so in a normal horizontal English page inline is width and block is height. They are arrays because a fragmented element can have several boxes; in practice you read index `[0]`. `devicePixelContentBoxSize` gives the content box in physical device pixels, which is what you want when a fractional CSS size must map to an integer pixel buffer. The `box` option in `observe(el, { box: 'border-box' })` only selects which box the observer watches for changes.
code
javascript · 12 linesconst el = document.createElement('div');
el.style.cssText = 'width:300px;padding:10px;border:2px solid;box-sizing:content-box';
document.body.append(el);
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
console.log('contentRect.width', entry.contentRect.width); // 300
console.log('content inline', entry.contentBoxSize[0].inlineSize); // 300
console.log('border inline', entry.borderBoxSize[0].inlineSize); // 324
}
});
ro.observe(el, { box: 'border-box' });go deeper
Recognise that the entry gives you numbers directly, and that the safe modern read is entry.contentBoxSize[0].inlineSize. Know that contentRect measures the content box, inside padding and border.
Explain the content-box versus border-box distinction with a concrete arithmetic example, say why the size properties are arrays, and state clearly that the box option selects the trigger rather than the payload.
Show when the choice of observed box prevents a feedback loop — for instance watching the border box when your callback writes padding — and know that box is per-observation, so one observer can mix conventions across targets.
Own the convention: pick one reading style for the codebase, in logical axes, so components survive a writing-mode change, and decide where a device-pixel-accurate measurement is worth the extra support surface.
## Four ways to describe one element's size A `ResizeObserverEntry` describes one element at one moment, and it does so four times over. This looks redundant until you notice that they answer different questions: which box, in which axis convention, and in which unit. ## contentRect — the legacy one `entry.contentRect` is a `DOMRectReadOnly`. Its `width` and `height` are the **content box** — the area inside padding and border. It is the property the first shipping version of the API exposed, and it is kept for compatibility. Two traps live here. First, `contentRect.top` and `contentRect.left` are **not** the element's position on screen; per spec they carry the element's padding-top and padding-left. Reading them expecting viewport coordinates is a classic misread — that is `getBoundingClientRect()`'s job. Second, it always describes the content box, no matter what you passed to `observe()`. ## contentBoxSize and borderBoxSize — the modern pair These are arrays of `ResizeObserverSize`, each with two numbers: ```javascript const { inlineSize, blockSize } = entry.borderBoxSize[0]; ``` `inlineSize` and `blockSize` are **writing-mode relative**. In `writing-mode: horizontal-tb` — English, and most of the web — inline is horizontal (width) and block is vertical (height). In a vertical writing mode they swap. That is a feature, not an annoyance: code written in logical terms keeps working when the writing mode changes. They are arrays because the spec allows for a fragmented element to report several boxes, for instance across multi-column fragments. No engine currently returns more than one, so `[0]` is the correct read — but write the index, because the property is a list. Very early implementations exposed a single object here rather than an array, which is why you still see defensive code of the shape `const size = entry.borderBoxSize[0] ?? entry.borderBoxSize;` in libraries that support old browsers. The difference between the two is the same difference CSS draws: the border box includes padding and border, the content box does not. For an element that is 300px wide with `padding: 10px` and `border: 2px`, the content box inline size is 300 and the border box inline size is 324, assuming the default `box-sizing: content-box`. ## devicePixelContentBoxSize This reports the content box in **physical device pixels** rather than CSS pixels. It is not simply the CSS size multiplied by `devicePixelRatio`: it accounts for how the element's fractional layout position rounds onto the device pixel grid, which is exactly the discrepancy that produces a blurry or half-pixel-off rendering when you size a pixel buffer by hand. It is the newest of the four and the last to reach all engines, so feature-detect before relying on it — passing an unsupported box value to `observe()` throws a `TypeError`, so a `try`/`catch` around the call is the usual detection. ## The box option controls what triggers, not what you get This is the part interviewers actually probe: ```javascript ro.observe(el, { box: 'border-box' }); ``` The option is one of `'content-box'` (the default), `'border-box'` or `'device-pixel-content-box'`, and it decides **which box the observer watches for changes**. It does not change the element's CSS `box-sizing`, and it does not narrow what the entry contains — you still receive `contentRect`, `contentBoxSize`, `borderBoxSize` and `devicePixelContentBoxSize` on every entry. Why does the choice matter? Because it changes when you are notified. Suppose an element's `padding` grows by 4px while its border box stays fixed at 324px. Watching the content box, that is a change and you get a callback. Watching the border box, nothing changed and you get nothing. The reverse holds when `box-sizing: content-box` and only the border width changes. Pick the box whose stability actually matters to your code — and if your callback writes padding or border, watching the border box is often what keeps you out of a feedback loop. One observer can mix conventions: `box` is an argument to `observe()`, not to the constructor, so a single observer may watch element A by border box and element B by content box. ## Which one to read Read `contentBoxSize[0]` or `borderBoxSize[0]` for new code; they are the maintained pair and they are writing-mode correct. Reach for `contentRect` only when supporting something old, and never for position. Reach for `devicePixelContentBoxSize` when a CSS-pixel measurement has to become an exact integer buffer size.
- You pass box: 'border-box' to observe(). Does entry.contentRect still contain a value?Yes. The `box` option only selects which box's changes count as a resize worth notifying you about. Every entry is built with `contentRect`, `contentBoxSize`, `borderBoxSize` and `devicePixelContentBoxSize` populated, so you can observe by one box and read another. What changes is *when* the callback runs, not what it receives.
- Why are contentBoxSize and borderBoxSize arrays rather than plain objects?The specification allows a fragmented element — for example split across multi-column fragments — to report a size per fragment, so the property is defined as a list. No engine returns more than one entry today, so reading `[0]` is correct in practice, but the array shape is why code that reads `entry.borderBoxSize.inlineSize` gets `undefined`.
- When would inlineSize not be the element's width?When the writing mode is vertical. `inlineSize` and `blockSize` are logical: inline follows the text direction and block is perpendicular to it. Under `writing-mode: vertical-rl`, inline size is the element's height in physical terms. Using the logical names is what lets one component work in both without branching.
saying these in an interview costs you the question
- Thinks box: 'border-box' means only borderBoxSize is populated
- Reads entry.borderBoxSize.inlineSize without indexing [0]
- Believes contentRect.top is the element's viewport position
- Assumes inlineSize is always the physical width
- Treats the box option as changing CSS box-sizing