ESC

Type to search the knowledge base.

ResizeObserver

Observe element size changes without window.resize — box options, loop limits, disconnect, and chart/layout patterns.

intermediate3 min read
  • javascript
  • resizeobserver

window.resize only cares about the viewport. Components need to know when their box changed — sidebar collapse, flex reflow, container queries via JS for charts. ResizeObserver fires when observed elements’ size changes.

const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    // entry.borderBoxSize / contentBoxSize / devicePixelContentBoxSize (arrays in spec)
    chart.resize(width, height);
  }
});

ro.observe(container);
// ro.unobserve(container);
// ro.disconnect();

Entry fields

function sizeOf(entry) {
  // Prefer box-size APIs when available
  const box = entry.borderBoxSize?.[0];
  if (box) {
    return { width: box.inlineSize, height: box.blockSize };
  }
  return {
    width: entry.contentRect.width,
    height: entry.contentRect.height,
  };
}

observe(el, { box: 'border-box' }) controls which box is watched (content-box, border-box, device-pixel-content-box).

Why not just offsetWidth in layout effect?

You can measure once after mount, but later size changes (font load, sibling flex, async content) won’t notify you. ResizeObserver covers the ongoing cases without polling.

Observation delivery

Callbacks are delivered asynchronously (like other observers), batched. Don’t assume they run before paint of the frame that changed size — design updates to be tolerant.

Resize loop errors

If your callback changes size of an observed element in a way that feeds back, browsers can report ResizeObserver loop limit exceeded (often a window error event, non-fatal). Patterns:

const ro = new ResizeObserver((entries) => {
  const { width } = sizeOf(entries[0]);
  // Write to a child, not the observed root, when possible
  canvas.width = width * devicePixelRatio;
});
ro.observe(wrapper); // wrapper size driven by layout, not by canvas width alone

Avoid: observe el → set el.style.height based on measurement → infinite churn.

Cleanup

useEffect(() => {
  const ro = new ResizeObserver(handler);
  ro.observe(ref.current);
  return () => ro.disconnect();
}, []);

Leaking observers keeps elements and closures alive — SPA leak classic.

vs container queries / media queries

CSS container queries handle many pure-style cases without JS. Use ResizeObserver when you need imperative reaction (canvas, third-party chart libs, virtualization measurements).

Interview answer (out loud)

“ResizeObserver notifies when an element’s box changes, which window.resize can’t do for internal layout. I read contentRect or borderBoxSize, disconnect on teardown, and avoid feedback loops that resize the observed node from the callback. For pure CSS responses I prefer container queries.”

Debouncing observer spam

Some layouts fire many observations during drag-resize. Debounce the expensive work, not the observer registration:

let raf = 0;
const ro = new ResizeObserver((entries) => {
  cancelAnimationFrame(raf);
  raf = requestAnimationFrame(() => {
    applySize(sizeOf(entries[0]));
  });
});

Coalesce to one update per frame.

Device pixel box

ro.observe(canvas, { box: 'device-pixel-content-box' });

Useful when backing store resolution must match physical pixels for sharp canvas drawing. Feature-detect: older engines may not support that box.

Testing

In unit tests, jsdom historically lacked ResizeObserver — polyfill or mock:

class RO {
  observe() {}
  unobserve() {}
  disconnect() {}
}
global.ResizeObserver = RO;

Integration tests in real browsers catch the feedback-loop class of bugs.

Further reading

Related guides