content-box and border-box observation and the contentBoxSize/borderBoxSize entry fields are supported in all current engines; device-pixel-content-box and devicePixelContentBoxSize are supported in Chromium and Firefox but not in Safari, so feature-detect the device-pixel option with try/catch and fall back to rounding the CSS size times devicePixelRatio.

Problem / Scenario Context

A mapping library renders tiles into a canvas and sizes its backing store from ResizeObserver. The team adopted device-pixel-content-box after reading that it avoids blurry canvases on fractional-DPR screens, and it works perfectly in Chrome. In Safari the whole map fails to initialise: observe() throws a TypeError for the unknown enum value, the exception escapes the setup function, and the map container stays empty.

Box options arrived later than the observer itself, and not everywhere at once. The Browser Compatibility & Polyfills topic covers overall support; this page covers the box options specifically.

Mechanics Explanation

ResizeObserver.observe(target, { box }) accepts a ResizeObserverBoxOptions enum value:

  • content-box (default) — deliver when the content box changes.
  • border-box — deliver when the border box changes.
  • device-pixel-content-box — deliver when the content box in device pixels changes; this includes zoom and moving the window between monitors with different pixel ratios, even if the CSS size is unchanged.

Because box is an enum, an unknown value is a WebIDL type error: the call throws synchronously. That is different from unknown dictionary members, which are ignored. On the entry side, fields that an engine does not support are simply undefined.

The first shipped version of the API had only contentRect; contentBoxSize and borderBoxSize followed, and Firefox initially exposed them as a single object rather than an array before aligning with the specification. Code that must run on older engines should handle all three shapes.

Box Option Support by EngineA grid of box options and entry fields against three engine families. Chromium supports content-box, border-box, device-pixel-content-box and all three size arrays. Firefox supports all of them as well. Safari supports content-box, border-box and the content and border size arrays, but not the device-pixel option or its size field.ChromiumFirefoxSafaricontent-box / border-boxyesyesyescontentBoxSize / borderBoxSizeyesyesyesdevice-pixel-content-boxyesyesno, throwsdevicePixelContentBoxSizeyesyesundefined

Comparison Table: Failure Modes

Unsupported feature What happens Detection Fallback
box: 'device-pixel-content-box' observe() throws TypeError try/catch a probe observe observe content-box
devicePixelContentBoxSize field undefined check the field on the entry Math.round(css × dpr)
contentBoxSize field (very old engines) undefined prototype check contentRect
contentBoxSize as object not array (old Firefox) [0] is undefined Array.isArray use the object directly
DPR change without CSS change no entry on content-box n/a listen to matchMedia('(resolution: …)')

Minimal Reproducible Example

TypeScript
function initMap(canvas: HTMLCanvasElement): void {
  const ro = new ResizeObserver(([e]) => {
    const dp = e.devicePixelContentBoxSize[0];          // undefined in Safari → TypeError
    canvas.width = dp.inlineSize;
    canvas.height = dp.blockSize;
    drawTiles();
  });
  ro.observe(canvas, { box: 'device-pixel-content-box' });   // throws in Safari
}

declare function drawTiles(): void;

Production-Safe Solution

TypeScript
function supportsDevicePixelBox(): boolean {
  const ro = new ResizeObserver(() => {});
  try { ro.observe(document.createElement('div'), { box: 'device-pixel-content-box' }); return true; }
  catch { return false; }
  finally { ro.disconnect(); }
}

function firstSize(v: ReadonlyArray<ResizeObserverSize> | ResizeObserverSize | undefined) {
  return Array.isArray(v) ? v[0] : (v as ResizeObserverSize | undefined);
}

export function observeCanvas(canvas: HTMLCanvasElement, draw: () => void): () => void {
  const useDevicePx = supportsDevicePixelBox();

  const apply = (e: ResizeObserverEntry): void => {
    const dp = firstSize(e.devicePixelContentBoxSize);
    const css = firstSize(e.contentBoxSize);
    const cssW = css?.inlineSize ?? e.contentRect.width;
    const cssH = css?.blockSize ?? e.contentRect.height;
    const w = dp?.inlineSize ?? Math.round(cssW * devicePixelRatio);
    const h = dp?.blockSize ?? Math.round(cssH * devicePixelRatio);
    if (canvas.width !== w || canvas.height !== h) {
      canvas.width = w;
      canvas.height = h;
      draw();
    }
  };

  const ro = new ResizeObserver((entries) => entries.forEach(apply));
  ro.observe(canvas, { box: useDevicePx ? 'device-pixel-content-box' : 'content-box' });

  // Without the device-pixel box, DPR changes (zoom, moving monitors) deliver no entry.
  let mq: MediaQueryList | null = null;
  const onDpr = (): void => {
    mq?.removeEventListener('change', onDpr);
    mq = matchMedia(`(resolution: ${devicePixelRatio}dppx)`);
    mq.addEventListener('change', onDpr);
    const r = canvas.getBoundingClientRect();
    apply({ contentRect: r, contentBoxSize: undefined, devicePixelContentBoxSize: undefined } as unknown as ResizeObserverEntry);
  };
  if (!useDevicePx) onDpr();

  return () => { ro.disconnect(); mq?.removeEventListener('change', onDpr); };
}

Where the device-pixel box is available, the browser tells you the exact snapped size and notifies you on zoom. Where it is not, the fallback rounds the CSS size times devicePixelRatio — occasionally one pixel off from the browser's own snapping, which is rarely visible — and a resolution media query listener catches DPR changes that content-box observation would miss.

Layered Canvas SizingFour boxes. Detect the device-pixel box option once. If supported, observe that box and use the exact device-pixel sizes. If not, observe the content box and round the CSS size times devicePixelRatio. In the fallback path, a resolution media query listener catches zoom and monitor changes that deliver no resize entry.Detect oncetry observe with the dpboxSupportedexact device-pixel sizesNot supportedround CSS ×devicePixelRatioFallback extraresolution media queryfor DPR

How Much the Fallback Costs You

The practical question is whether the fallback is visibly worse. For most canvases it is not; for some it is:

  • Integer DPR (1, 2, 3) — CSS sizes that are whole pixels multiply exactly, and rounding matches the browser's snapping. No difference.
  • Fractional DPR (1.25, 1.5, 2.625) — common on Windows laptops and some Android phones. A canvas at a fractional CSS position can be snapped one device pixel differently from the rounded value, producing a backing store that is one pixel too wide or narrow and is scaled by a hair — visible as slight blur on thin lines and text.
  • Pixel-art and charts with 1px lines — the most sensitive; a one-pixel mismatch makes lines alternate between sharp and soft.

Safari's desktop and iOS devices use integer ratios, which is part of why the missing option rarely produces visible artefacts there. The deep dive in using devicePixelContentBox for crisp canvas shows the snapping arithmetic.

Backing-Store Width Error by Device Pixel RatioA bar chart of the typical difference between a rounded CSS-times-DPR backing store width and the browser's snapped width. At a device pixel ratio of one or two there is no difference. At 1.25 and 1.5 there is occasionally a one pixel difference. At 2.625 the difference is also up to one pixel.Max width error of the fallback, canvas at a fractional CSS offsetDPR 10 pxDPR 20 pxDPR 1.25up to 1 pxDPR 1.5up to 1 pxDPR 2.625up to 1 px

Verification Steps

  • Run supportsDevicePixelBox() in each target browser and confirm it matches the support grid.
  • Zoom the page in Safari and confirm the fallback's media query listener resizes the canvas.
  • Move a window between monitors with different DPRs and confirm the canvas stays crisp.
  • Test on a 1.25× or 1.5× Windows display and inspect thin lines for blur in the fallback path.
  • Confirm initialisation never throws by running the setup in Safari with the console's pause-on-exceptions enabled.

Common Mistakes to Avoid

  • Passing the device-pixel box unguarded. It throws in Safari.
  • Reading devicePixelContentBoxSize[0] without a check. It is undefined where unsupported.
  • Forgetting DPR changes in the fallback. content-box observation does not notice zoom.
  • Using Math.floor or Math.ceil. Rounding is closest to how browsers snap.

FAQ

Does Safari support device-pixel-content-box?

Not at the time of writing. Safari supports content-box and border-box observation and the corresponding size arrays, but passing device-pixel-content-box throws, and entries have no devicePixelContentBoxSize.

Why does an unsupported box value throw instead of being ignored?

Because box is an enumeration. WebIDL treats an unknown enum value as a type error, while unknown dictionary members are ignored.

Do I need border-box support detection?

Not for current browsers; content-box and border-box are universally supported. Only very old engines lacked the box option entirely, and there it was ignored rather than throwing.

Is the rounding fallback good enough?

For integer device pixel ratios it is exact. For fractional ratios it can be one device pixel off, which matters only for very thin lines or pixel art.

Why does the fallback need a resolution media query?

Because content-box observation only fires when the CSS size changes. Browser zoom and moving a window to a monitor with a different pixel ratio change the device-pixel size without changing the CSS size, so without the media query the canvas keeps a stale backing store and looks blurry until the next real resize.

Should I observe border-box for a canvas?

No. A canvas's drawing buffer maps onto its content box. Borders and padding sit outside the drawing surface, so sizing the backing store from the border box would stretch the drawing slightly.

Is it worth detecting support on every page load?

The probe costs one observer construction and one observe call, which is negligible. Cache the result for the page's lifetime so every canvas on the page reuses it instead of probing again.

Can a polyfill add device-pixel-content-box?

Not accurately. The value depends on the browser's internal pixel snapping, which script cannot observe directly. The fallback approximates it, which is as close as a polyfill could get.


↑ Back to Browser Compatibility & Polyfills for Observer APIs