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