Skip to content
R.

When should I use contentBoxSize vs borderBoxSize in ResizeObserverEntry?

Choose the ResizeObserverEntry field that matches the edge you are actually testing: layout size, border-inclusive size, or device-pixel size. This article maps box sizing, transforms, and scrollbars to the property that best answers each diagnostic question.

About 11 min readComments

ResizeObserver is easy to misuse when you expect one field to answer every sizing question. It does not. The API reports different boxes, and those boxes stay in layout space even when the element is transformed or when the device has a different pixel ratio. That is why a test can feel “wrong” even when the browser is doing exactly what the specification says.

This article gives a small diagnostic rule: pick `contentBoxSize` when the content area matters, `borderBoxSize` when the full layout footprint matters, `devicePixelContentBoxSize` when you need device pixels, and `contentRect` only as a legacy fallback. It also shows why transforms and scrollbar changes are separate concerns, so a layout test can ask the right question instead of comparing the wrong measurement to the visible result.

Start with the question the browser is answering

The easiest way to misuse ResizeObserver is to treat its entry as a single universal measurement. It is not. The observer gives you size information about the observed element, but each field describes a different box or a different unit system. If you read the wrong one, the numbers can look inconsistent even when the browser is behaving correctly.

That mismatch becomes obvious in layout tests. A component may grow by CSS pixels, the border may stay fixed, a transform may visually scale the element without changing its layout box, and a high-DPI screen may report a different number of device pixels than CSS pixels. A developer looking only at one failing assertion can conclude that ResizeObserver is unreliable when the real problem is that the test asked the wrong measurement question.

The fixture in this article turns that ambiguity into a decision rule. It separates the three questions that usually get mixed together: what is the content box, what is the full border box, and what does the visual result look like after scaling or device-pixel conversion? Once those are separated, the documented properties are easier to choose between, and the remaining edge cases are easier to explain in a test failure.

main.mjs / javascript
export function computeBoxSizes({width, padding = 0, border = 0, devicePixelRatio = 1} = {}) {
  // All inputs are CSS pixels. We return the three ResizeObserverEntry-style sizes
  // as simple objects: {inlineSize, blockSize} in CSS pixels for content/border
  // and in device pixels for devicePixelContentBoxSize.
  const contentInline = Math.max(0, width - padding * 2 - border * 2);
  const borderInline = Math.max(0, width);

  const contentBoxSize = {inlineSize: contentInline, blockSize: 0};
  const borderBoxSize = {inlineSize: borderInline, blockSize: 0};
  const devicePixelContentBoxSize = {
    inlineSize: Math.round(contentInline * devicePixelRatio),
    blockSize: 0,
  };

  return {contentBoxSize, borderBoxSize, devicePixelContentBoxSize};
}

export function pickResizeObserverProperty({
  wants = "layout", // "layout" means the untransformed CSS layout edge
  boxSizing = "content-box", // "content-box" or "border-box"
  transformScale = 1, // CSS transform scale applied to element (visual only)
  devicePixelRatio = 1,
  measured = {}, // optional precomputed sizes from computeBoxSizes()
} = {}) {
  // This function implements a deterministic, testable policy mapping a
  // requested measurement (layout vs visual vs device pixels) and the
  // element's box-sizing/transform/DPR to the ResizeObserver-derived
  // property a test should read — or a note that ResizeObserver doesn't
  // provide that value (call getBoundingClientRect instead).

  // We do not run inside a browser here; instead this is a pure algorithmic
  // decision helper. Real browser measurements may differ slightly.

  // Normalize inputs
  const wantsNormalized = wants;
  const boxNorm = boxSizing === "border-box" ? "border-box" : "content-box";
  const scale = Number(transformScale) || 1;
  const dpr = Number(devicePixelRatio) || 1;

  // If caller asked for device pixels of the content box, recommend
  // devicePixelContentBoxSize when available (modern browsers).
  if (wants === "device-pixels-content") {
    return {property: "devicePixelContentBoxSize", reason: "content box in device pixels"};
  }

  // If they want the layout edge (untransformed): choose content vs border.
  if (wantsNormalized === "layout") {
    if (boxNorm === "border-box") {
      return {property: "borderBoxSize", reason: "box-sizing: border-box — borderBoxSize matches layout edge"};
    }
    return {property: "contentBoxSize", reason: "box-sizing: content-box — contentBoxSize matches layout edge"};
  }

  // If they want the visual edge after transforms, ResizeObserver entry
  // properties report layout boxes (pre-transform). A transform scale
  // applied only affects painting; to measure visual size use
  // getBoundingClientRect (not a ResizeObserverEntry property).
  if (wantsNormalized === "visual") {
    if (scale !== 1) {
      return {property: null, reason: "element is transformed; use element.getBoundingClientRect() — ResizeObserver box sizes are layout boxes and ignore transforms"};
    }
    // No transform: visual == layout, so fall back to layout logic.
    return pickResizeObserverProperty({wants: "layout", boxSizing: boxNorm, devicePixelRatio: dpr});
  }

  // Fallback
  return {property: null, reason: "unknown request"};
}

SourcesMDN: ResizeObserver (opens a new tab)MDN: ResizeObserverEntry (opens a new tab)

`contentBoxSize` and `borderBoxSize` measure different things

MDN’s box-model summary is the key distinction: the content box is the area inside padding and border, while the border box includes content, padding, and border. That is not an implementation detail; it is the contract that makes the two properties useful for different kinds of diagnostics. If your layout logic should follow the usable content area, content box is the right target. If the thing you care about is the full element edge as it contributes to layout, border box is the better match.

The supplied helper mirrors that distinction by computing a content inline size and a border inline size from the same width, padding, and border inputs. That is a useful reminder for tests: one element can have two correct sizes at the same time. The values are not conflicting; they are answering different questions about the same box.

This is especially important when `box-sizing: border-box` is in play. With border-box sizing, the CSS width you set is already the border edge. The content area becomes smaller once padding and border are subtracted. In that situation, a test that expects the visible outer edge should read the border box, not the content box. A test that expects the area available for text should read the content box.

SourcesMDN: ResizeObserver (opens a new tab)

How the entry properties map to real layout edge cases

`ResizeObserverEntry` exposes four relevant size-related properties, but they do not all serve the same role. `contentBoxSize` reports the content box, `borderBoxSize` reports the border box, `devicePixelContentBoxSize` reports the content box in device pixels, and `contentRect` remains for backward compatibility. MDN specifically notes that `contentRect` is now a legacy property. That means it can still be useful as a compatibility fallback, but it should not be your first choice when newer box-specific data is available.

The article’s decision helper reflects that ordering. When the reader wants the layout edge, it chooses between `contentBoxSize` and `borderBoxSize` depending on the box model. When the reader wants device-pixel precision, it recommends `devicePixelContentBoxSize`. And when the reader wants the visual size after transforms, it explicitly returns no ResizeObserver property and points away from the API, because transforms are a separate question.

That last branch matters. A transform changes the rendered appearance of the element, but the observer properties describe layout boxes, not post-transform paint geometry. A scale of 2 can make the element look larger without changing the box-size values you should use for layout logic. The correct response is not to force one ResizeObserver field to behave like another; it is to switch to a visual geometry API for that specific question.

SourcesMDN: ResizeObserverEntry (opens a new tab)W3C: Resize Observer — transform notifications (opens a new tab)W3C: CSS Transforms — rendering and client rectangles (opens a new tab)

Why transforms, box sizing, and scrollbars need separate checks

These three layout influences often get blamed on the same metric, but they affect the result in different ways. Box sizing changes which box the CSS width describes. Transforms change the visual appearance after layout. For scrollbar cases, specify the overflow condition and the available-space expectation separately; this policy helper does not reproduce browser scrollbar geometry. None of those effects automatically make one ResizeObserver entry field wrong; they change which field is relevant to the question you are asking.

The most common error is to test a transformed element with a layout-box assertion. If the test wants the displayed size, a border box or content box value will remain pre-transform and can look wrong compared with the screen. That is expected. The observer is not broken. It is describing layout, not painted scale. The opposite mistake also happens: a developer expects the border box to include the effect of padding or assumes content-box numbers should equal the CSS width even when `box-sizing: border-box` shrinks the inner area.

Scrollbars add another layer of confusion because they can appear only after overflow occurs, which means the size you thought was stable may change once the browser allocates scrollbar space. In tests, that usually means the assertions should specify whether the observable behavior is the content edge or the outer edge, and whether scrollbar appearance is part of the scenario. If it is, the test needs to pin the overflow condition instead of relying on a generic “same width” expectation.

SourcesMDN: ResizeObserver (opens a new tab)MDN: ResizeObserverEntry (opens a new tab)W3C: Resize Observer — transform notifications (opens a new tab)W3C: CSS Transforms — rendering and client rectangles (opens a new tab)

Read the helper as a policy, not a measurement engine

The supplied implementation is not trying to simulate the browser. It is a pure policy helper that turns an intent into a property choice. That is valuable because it separates the reasoning step from the browser step. Instead of asking, “What number did ResizeObserver happen to emit?” the helper asks, “What question does the test need answered?” The result is deterministic: layout edge, visual edge, or device-pixel content edge each produce a different recommendation.

That distinction is why the transform branch returns `null` instead of forcing a property name. A good diagnostic does not pretend that every problem belongs to the same API. When the element is visually scaled, the helper says that ResizeObserver box sizes are the wrong tool for that measurement and that a visual geometry read is needed instead. That is the right kind of failure in a test helper: it tells you when the contract does not match the question.

The helper also makes the device-pixel case explicit. On a high-DPI display, a device-pixel content size is not the same as a CSS-pixel content size. If your rendering logic or canvas alignment depends on device pixels, reading the CSS-pixel field can undercount the real physical size. Using the device-pixel field avoids that mismatch, but only when the browser supports it and when the test truly needs physical-pixel precision rather than layout precision.

SourcesMDN: ResizeObserverEntry (opens a new tab)

How to decide which property to read in tests

A useful test starts by naming the edge it is supposed to verify. If the question is “Did the content area shrink?” then `contentBoxSize` is the direct match. If the question is “Did the element’s outer layout footprint change?” then `borderBoxSize` is the better assertion. If the question is “Did we render at device-pixel precision?” then `devicePixelContentBoxSize` is the one to prefer. And if the question is “How big does it look after transform?” then the answer is not one of these properties at all.

That is the practical value of the helper and the accompanying fixture shape: they force the reader to choose a measurement by intent instead of by convenience. This is especially helpful in regression tests, where a passing assertion can accidentally check the wrong edge and still give false confidence. A content-box test may pass while a border-box bug remains visible, or vice versa.

A second useful habit is to keep the negative case visible. The article’s transform branch is one. Another is to treat `contentRect` as a fallback rather than the preferred path. The API still supports it, but MDN marks it as legacy, so a modern test should use it only when the newer box-size fields are unavailable. That framing keeps the article honest: the right property depends on the measurement target, not on a preference for one field name over another.

main.test.mjs / javascript
import test from 'node:test';
import assert from 'node:assert/strict';
import {computeBoxSizes, pickResizeObserverProperty} from './main.mjs';

// Success case: box-sizing: border-box
test('border-box: recommend borderBoxSize for layout edge', () => {
  const sizes = computeBoxSizes({width: 200, padding: 10, border: 2, devicePixelRatio: 1});
  // contentInline should be width - padding*2 - border*2 = 200 - 20 - 4 = 176
  assert.equal(sizes.contentBoxSize.inlineSize, 176);
  assert.equal(sizes.borderBoxSize.inlineSize, 200);

  const decision = pickResizeObserverProperty({wants: 'layout', boxSizing: 'border-box'});
  assert.equal(decision.property, 'borderBoxSize');
  assert.match(decision.reason, /borderBoxSize/);
});

// Failure / boundary case 1: visual measurement after transform — ResizeObserver
// contentBoxSize/borderBoxSize do NOT reflect visual scale. The helper must
// indicate that ResizeObserver properties are insufficient.
test('transform: scale — recommend getBoundingClientRect (not a ResizeObserver property)', () => {
  const decision = pickResizeObserverProperty({wants: 'visual', boxSizing: 'content-box', transformScale: 2});
  // Because transformScale !== 1, none of the ResizeObserverEntry size
  // properties represent the visual (post-transform) size.
  assert.equal(decision.property, null);
  assert.match(decision.reason, /getBoundingClientRect/);
});

// Failure / boundary case 2: device pixel content request — recommend devicePixelContentBoxSize
test('device pixel content: recommend devicePixelContentBoxSize', () => {
  const sizes = computeBoxSizes({width: 120.5, padding: 5.25, border: 1, devicePixelRatio: 2});
  // compute expected device-pixel content inline size
  const expectedContent = Math.max(0, 120.5 - 5.25 * 2 - 1 * 2);
  const expectedDevicePixels = Math.round(expectedContent * 2);
  assert.equal(sizes.devicePixelContentBoxSize.inlineSize, expectedDevicePixels);

  const decision = pickResizeObserverProperty({wants: 'device-pixels-content', boxSizing: 'content-box', devicePixelRatio: 2});
  assert.equal(decision.property, 'devicePixelContentBoxSize');
  assert.match(decision.reason, /device pixels/);
});

// Extra boundary: unknown wants should return null property
test('unknown wants -> null and explanatory reason', () => {
  const decision = pickResizeObserverProperty({wants: 'something-else'});
  assert.equal(decision.property, null);
  assert.match(decision.reason, /unknown request/);
});

SourcesMDN: ResizeObserver (opens a new tab)MDN: ResizeObserverEntry (opens a new tab)

Share LinkedIn Email Subscribe

Discussion

Leave a comment

Comments appear after review. No email needed.

Image preview

Follow the blog

Choose email updates or your favorite RSS reader.

The weekly email

New articles on code, design, and AI. Fridays, only when there is something new.

By subscribing, you agree to weekly blog emails.
Confirm in your inbox. Unsubscribe anytime. Privacy

Use OpenRSS

Preview the feed, then choose a reader to subscribe.

Open in OpenRSS (opens a new tab)

Already have a reader?

Paste this link into your reader's Add feed option.

Prefer a reader? RSS does not require an email address. View XML feed