JavaScriptResize Observer

ResizeObserver

The window.resize event tells you when the viewport changes size. It says nothing about individual elements, which is what you usually actually need — a card whose contents grew, a sidebar a user is dragging, an iframe whose content loaded. ResizeObserver fires a callback whenever an observed element's size changes, regardless of cause, batched per frame so there is no jank. It is the modern answer to "container queries before container queries existed".

The basic shape

JS
const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    entry.target.dataset.size = `${Math.round(width)}x${Math.round(height)}`;
  }
});

ro.observe(document.querySelector(".card"));

One observer can watch many elements. observe(el) starts watching; unobserve(el) stops; disconnect() clears all.

The entry object
  • target — the observed element.

  • contentRect — a DOMRectReadOnly with x, y, width, height. This is the content box, excluding borders and padding (similar to getBoundingClientRect minus those).

  • borderBoxSize — array of { inlineSize, blockSize } for the border box.

  • contentBoxSize — same shape for the content box.

  • devicePixelContentBoxSize — same in physical pixels (HiDPI-aware).

The newer borderBoxSize / contentBoxSize arrays handle multi-column writing modes (inlineSize is width in horizontal text, height in vertical text). Use them in new code; contentRect is fine for plain web pages.

Choosing what to observe

JS
ro.observe(el);
ro.observe(el, { box: "border-box" });        // total size including padding/border
ro.observe(el, { box: "content-box" });       // inner content size (default)
ro.observe(el, { box: "device-pixel-content-box" });
One observer, one box per target
Calling `observe(el)` again with a different `box` replaces the previous observation for that element on that observer.
The classic use case: faux container queries

Adapt a component's class based on its own width, not the viewport's. (Native CSS container queries cover this now, but ResizeObserver still works everywhere.)

card-layout.js

JS
const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const w = entry.contentRect.width;
    entry.target.classList.toggle("compact", w < 320);
    entry.target.classList.toggle("wide", w > 720);
  }
});

document.querySelectorAll(".card").forEach((c) => ro.observe(c));
Resizable panels and textareas

auto-resize a textarea relative to a paired preview

JS
const editor = document.querySelector("textarea");
const preview = document.querySelector(".preview");

new ResizeObserver(([entry]) => {
  preview.style.height = `${entry.contentRect.height}px`;
}).observe(editor);
Watching an iframe

A common painful problem: an iframe's content height changes and your parent layout does not know. Observe the iframe element from the parent (you can only see its outer size, not the inner DOM) for cross-origin pages, or observe the inner document.documentElement from a same-origin iframe and postMessage the height up.

Avoiding loops
ResizeObserver loop limit
If your callback resizes the very element it is observing (or a sibling that re-flows it) the browser detects an infinite loop and emits the famous "ResizeObserver loop limit exceeded" warning. The browser is doing the right thing — it has skipped frames to avoid hanging. Restructure so the size you set is independent of the size you read, or guard with a "did the size really change" check.

JS
const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const w = Math.round(entry.contentRect.width);
    if (entry.target.dataset.lastW === String(w)) continue;
    entry.target.dataset.lastW = String(w);
    entry.target.classList.toggle("narrow", w < 400);
  }
});
ResizeObserver vs window.resize
  • Per-element vs per-window — ResizeObserver triggers regardless of cause; window resize only fires when the viewport changes.

  • Batched delivery — RO callbacks run once per frame; resize listeners fire repeatedly and need debouncing.

  • Works inside scrollable containers, flex/grid items that change because of content — none of which fires window.resize.

Cleanup

JS
ro.unobserve(el);    // one element
ro.disconnect();     // all elements

In single-page apps with frequently-mounted components, hold the observer in your component's scope and disconnect it on unmount — otherwise references keep elements alive and you leak memory.

Browser support
ResizeObserver is available everywhere relevant since 2020 — Chrome, Firefox, Safari, Edge. The newer `borderBoxSize` / `contentBoxSize` arrays landed slightly later than `contentRect`; feature-detect if you need to support old Safari.