Read contentBoxSize[0] or borderBoxSize[0] — the logical, writing-mode-aware sizes — matching the box you observe, use devicePixelContentBoxSize[0] for canvas backing stores, and treat contentRect as a legacy convenience that is always the content box in physical width/height.

Problem / Scenario Context

A design-system card adjusts its layout using entry.contentRect.width. It works everywhere until the product ships a Japanese vertical-text edition: in writing-mode: vertical-rl the card's "width" breakpoint now responds to its height, and layouts flip at the wrong sizes. Separately, a data-grid component sizes columns from contentRect and ends up 2px too narrow per column because the design uses a 1px border that contentRect excludes. And a chart component draws a blurry canvas because it multiplies contentRect.width by devicePixelRatio and rounds differently from how the browser snaps the canvas to device pixels.

All three read a size that does not describe what they need. The ResizeObserver Mechanics & Triggers topic covers when entries arrive; this page covers which number to take from them.

Mechanics Explanation

A ResizeObserverEntry carries four sizes:

  • contentRect — a DOMRectReadOnly of the content box, in physical width and height, with x/y set to the padding offsets. It exists for backward compatibility with the first version of the API.
  • contentBoxSize — an array of ResizeObserverSize objects, each with inlineSize and blockSize: the content box in logical axes. In horizontal writing modes, inline is width; in vertical modes, inline is height.
  • borderBoxSize — the same shape for the border box: content plus padding plus border.
  • devicePixelContentBoxSize — the content box in device pixels, snapped exactly as the browser will paint it. The only reliable input for a crisp canvas.

The sizes are arrays because the spec anticipates elements split into multiple fragments (multi-column layout, paginated media); today every browser returns exactly one entry, so [0] is correct.

Separately, observe(target, { box }) chooses which box change triggers delivery. Observing content-box (the default) delivers when the content box changes; border-box when the border box changes; device-pixel-content-box when the device-pixel size changes, which includes zoom and DPR changes that leave CSS sizes untouched. All four size fields are populated regardless of which box triggered the entry.

Four Sizes on One EntryFour stacked layers describing the sizes on a ResizeObserverEntry. contentRect is the content box in physical width and height, kept for compatibility. contentBoxSize is the content box in logical inline and block sizes. borderBoxSize adds padding and border in logical sizes. devicePixelContentBoxSize is the content box in device pixels, snapped the way it will be painted.contentRectContent box, physical width and height, x and y are padding offsets. Legacy.contentBoxSize[0]Content box as inlineSize and blockSize — follows writing-mode.borderBoxSize[0]Content + padding + border, logical axes — what the element occupies.devicePixelContentBoxSize[0]Content box in device pixels, snapped as painted — for canvas.

Comparison Table: Which Size for Which Job

Job Read Observe box Why
Responsive component breakpoints contentBoxSize[0].inlineSize content-box space available for content, writing-mode aware
Fitting into a parent slot borderBoxSize[0] border-box the footprint the element occupies
Canvas backing store devicePixelContentBoxSize[0] device-pixel-content-box exact device pixels; updates on zoom
Text reflow decisions contentBoxSize[0] content-box padding does not hold text
Legacy code, horizontal-only contentRect.width content-box compatible, but not writing-mode aware
Position within parent none — sizes only use getBoundingClientRect

Minimal Reproducible Example

TypeScript
const card = document.querySelector<HTMLElement>('.card')!;   // padding 16px, border 1px
new ResizeObserver(([e]) => {
  console.table({
    'contentRect.width': e.contentRect.width,
    'contentBox inline': e.contentBoxSize[0].inlineSize,
    'borderBox inline': e.borderBoxSize[0].inlineSize,
    'device px inline': e.devicePixelContentBoxSize?.[0].inlineSize,
  });
}).observe(card);

// Now switch the card to vertical text:
card.style.writingMode = 'vertical-rl';

In horizontal mode, contentRect.width equals the content inline size and the border box is 34px larger. After switching to vertical text, contentRect.width still reports the physical width while inlineSize now reports the physical height — the dimension text actually runs along.

Production-Safe Solution

TypeScript
export interface ElementSize {
  inline: number;         // content box, logical
  block: number;
  borderInline: number;   // footprint, logical
  borderBlock: number;
  devicePxInline?: number;
  devicePxBlock?: number;
}

export function readSize(e: ResizeObserverEntry): ElementSize {
  // Old engines shipped contentRect only; newer ones may return a single object, not an array.
  const first = <T,>(v: T | readonly T[] | undefined): T | undefined =>
    Array.isArray(v) ? v[0] : (v as T | undefined);

  const content = first(e.contentBoxSize);
  const border = first(e.borderBoxSize);
  const dp = first(e.devicePixelContentBoxSize);

  return {
    inline: content?.inlineSize ?? e.contentRect.width,
    block: content?.blockSize ?? e.contentRect.height,
    borderInline: border?.inlineSize ?? (e.target as HTMLElement).offsetWidth,
    borderBlock: border?.blockSize ?? (e.target as HTMLElement).offsetHeight,
    devicePxInline: dp?.inlineSize,
    devicePxBlock: dp?.blockSize,
  };
}

// Breakpoints on available content space, writing-mode aware:
const ro = new ResizeObserver((entries) => {
  for (const e of entries) {
    const { inline } = readSize(e);
    (e.target as HTMLElement).dataset.size = inline < 360 ? 's' : inline < 720 ? 'm' : 'l';
  }
});
ro.observe(card, { box: 'content-box' });

The fallbacks cover engines that predate the box-size arrays (and early Firefox, which exposed a single object instead of an array). Reading offsetWidth in the fallback is safe inside the callback because layout is clean at that point, but on current browsers the branch never runs.

For canvas, observe the device-pixel box so zoom changes deliver an entry even when the CSS size does not change:

TypeScript
const canvasRO = new ResizeObserver(([e]) => {
  const s = readSize(e);
  const canvas = e.target as HTMLCanvasElement;
  canvas.width = s.devicePxInline ?? Math.round(s.inline * devicePixelRatio);
  canvas.height = s.devicePxBlock ?? Math.round(s.block * devicePixelRatio);
  redraw(canvas);
});
try { canvasRO.observe(canvas, { box: 'device-pixel-content-box' }); }
catch { canvasRO.observe(canvas); }                    // engines without the box option

declare const canvas: HTMLCanvasElement;
declare function redraw(c: HTMLCanvasElement): void;

The details of pixel snapping are in using devicePixelContentBox for crisp canvas.

Physical Versus Logical Sizes in Vertical TextTwo columns for a card in vertical-rl writing mode. Physical contentRect reports width as the short horizontal dimension and height as the long vertical one, so a width breakpoint reacts to the wrong axis. Logical contentBoxSize reports inlineSize along the direction text runs, which is vertical, so the same breakpoint code keeps working.contentRect (physical)width = horizontal extentIn vertical text, that is the block axisBreakpoints track the wrong dimensioncontentBoxSize (logical)inlineSize = the direction text runsCorrect in horizontal and vertical modesSame breakpoint code for every script

How the Observed Box Changes Delivery

Choosing the observed box is not only about which number you read; it decides which changes wake you up. Observing content-box means a padding change delivers nothing (the content box did not change) — handy when padding is animated and you only care about content space. Observing border-box delivers for padding and border changes too, which is right when the element's footprint matters to a parent layout.

That choice also interacts with the resize loop. A callback that toggles a class adding padding is harmless under content-box observation and can loop under border-box, because the write changes the observed size. When in doubt, observe the box whose size your callback reads, and write only properties outside it. The loop mechanics are covered in ResizeObserver and the update-the-rendering steps.

Which Changes Deliver an Entry for Each Observed BoxA grid of changes against the three observable boxes. A content width change delivers for all three. A padding change delivers only for border-box. A border change delivers only for border-box. A browser zoom that keeps CSS sizes delivers only for device-pixel-content-box. A device pixel ratio change, such as moving the window to another monitor, also delivers only for device-pixel-content-box.content-boxborder-boxdevice-pixel-content-boxContent width changesdeliversdeliversdeliversPadding changessilentdeliverssilentBorder changessilentdeliverssilentZoom, same CSS sizesilentsilentdeliversMoved to another monitorsilentsilentdelivers

Verification Steps

  • Log all four sizes for a padded, bordered element and confirm the differences match its CSS.
  • Switch to writing-mode: vertical-rl and confirm breakpoints still respond to the text direction.
  • Zoom the browser with a canvas observed via device-pixel-content-box and confirm an entry arrives and the canvas stays crisp.
  • Animate padding on an element observed with content-box and confirm no entries are delivered.
  • Run in an older browser to exercise the fallback path if you support one.

Common Mistakes to Avoid

  • Using contentRect in writing-mode-aware components. It is always physical.
  • Reading content size when the footprint matters. Borders and padding are excluded.
  • Multiplying CSS size by devicePixelRatio for canvas. The browser's snapping can differ by a pixel; use the device-pixel box.
  • Forgetting the array index. contentBoxSize.inlineSize is undefined; it is contentBoxSize[0].inlineSize.

FAQ

Why are contentBoxSize and borderBoxSize arrays?

To support elements split into multiple fragments, such as content flowing across columns. No browser fragments observed elements today, so the arrays always contain exactly one item.

Is contentRect deprecated?

It is kept for compatibility and is not going away, but the specification describes it as legacy. New code should use the box-size arrays, which are writing-mode aware.

Do I get borderBoxSize if I observe content-box?

Yes. All size fields are filled in on every entry. The observed box only decides which changes cause an entry to be delivered.

Is devicePixelContentBoxSize supported everywhere?

It is supported in Chromium and Firefox. Safari has lagged, so feature-detect it and fall back to rounding the CSS size multiplied by devicePixelRatio.

Which box should a container-query-like component observe?

The content box. CSS container queries size against the container's content box, so observing content-box and reading contentBoxSize[0].inlineSize keeps script breakpoints consistent with any container queries used alongside them.

Why does contentRect have non-zero x and y?

They are the padding offsets: the content box's position relative to the padding box's top-left corner. They say nothing about the element's position on the page.


↑ Back to ResizeObserver Mechanics & Triggers