DEV Community

Cover image for Pick which ResizeObserverEntry field your test should read
Chris Rodriguez
Chris Rodriguez

Posted on Originally published at chriseugenerodriguez.com

Pick which ResizeObserverEntry field your test should read

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"};
}
Enter fullscreen mode Exit fullscreen mode

Read the full article: https://chriseugenerodriguez.com/blog/resizeobserver-entry-property-diagnostics

Top comments (0)