If a ResizeObserver-based test fails, the problem is often not the browser but the measurement you asked for. ResizeObserverEntry exposes different boxes and units: contentBoxSize (content area), borderBoxSize (outer element footprint), and devicePixelContentBoxSize (content in device pixels). Each field answers a different diagnostic question.
Implementation decision
Start your test by naming the edge you intend to verify. The decision helper in the article is a tiny deterministic policy: it picks contentBoxSize when the question is about the available content area, borderBoxSize when the outer layout footprint matters, and devicePixelContentBoxSize when physical pixels matter. If the element is transformed (scale != 1), the helper returns a null recommendation and points to using a visual geometry API because ResizeObserver fields report layout boxes before transforms.
Expected behavior
- contentBoxSize will change when padding/border adjustments alter the usable inner space.
- borderBoxSize reflects changes to the element’s outer layout contribution (especially important with box-sizing: border-box).
- devicePixelContentBoxSize (when the browser supports it) provides rounded device-pixel counts useful for canvas alignment or pixel-perfect rendering.
- None of these ResizeObserverEntry fields reflect post-transform painted size. A CSS transform that scales an element affects visual appearance but not the entry’s box sizes.
Limitations
- devicePixelContentBoxSize availability depends on browser support; treat it as an opt-in precision tool rather than a default.
- ResizeObserver provides layout geometry, not painted geometry; for visual-size checks use getBoundingClientRect or similar.
- Scrollbar appearance can change which box is relevant; tests should explicitly include or pin overflow behavior when scrollbar presence matters.
The article includes a small pure-policy helper (no real browser calls) and a test fixture showing success and boundary cases (box-sizing: border-box, transform scale, device-pixel request). Use it to make your assertions ask the right question instead of comparing the wrong measurement to the visible result.
Working example
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"};
}
Read the full article: https://chriseugenerodriguez.com/blog/resizeobserver-entry-property-diagnostics
Top comments (0)