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
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 withx,y,width,height. This is the content box, excluding borders and padding (similar togetBoundingClientRectminus 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
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" });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
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
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
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
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.