What it observes
ResizeObserver watches one or more elements and fires a callback
whenever an observed element’s box dimensions change. The callback
receives an array of ResizeObserverEntry records, each with:
target— the observed element.contentRect— aDOMRectReadOnlyof the content box.borderBoxSize,contentBoxSize,devicePixelContentBoxSize— arrays (one entry per fragment, e.g. multi-column layouts).
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const { inlineSize, blockSize } = entry.contentBoxSize[0];
entry.target.classList.toggle('is-narrow', inlineSize < 480);
}
});
ro.observe(document.querySelector('.card'));
The observer fires once before the first frame after observation starts, then on every size change. Callbacks run in a microtask right before paint, so DOM mutations from the callback show up in the same frame.
Why it replaces window.onresize
A window.onresize listener fires only on viewport changes,
which is a poor proxy for component sizing. A card may resize
because:
- A sibling expanded (no viewport change).
- A parent’s grid track recalculated (no viewport change).
- A font finally loaded and reflowed text (no viewport change).
- The user toggled a developer-tools panel (no viewport change).
ResizeObserver catches every cause without polling, with about
0.2 ms overhead per fired callback per the Chromium 2023 perf benchmarks.
The three observation boxes
The constructor’s options accept a box value that selects which
size to report:
box | Reports |
|---|---|
content-box | The content area, exclusive of padding and border. (Default.) |
border-box | The full bordered area. |
device-pixel-content-box | The content area in actual device pixels (matters at non-integer DPR). |
Choose device-pixel-content-box when drawing a <canvas>: the
pixel-perfect size avoids blur on high-DPR displays.
const ro = new ResizeObserver((entries) => {
const { inlineSize, blockSize } = entries[0].devicePixelContentBoxSize[0];
canvas.width = inlineSize;
canvas.height = blockSize;
draw();
});
ro.observe(canvas, { box: 'device-pixel-content-box' });
Avoiding the loop-detection error
The callback can mutate the DOM. If the mutation changes an
observed element’s size, the observer would fire again, leading
to an infinite loop. The spec includes a loop limit: if a
single resize cycle iterates more than once, the engine fires a
ResizeObserver loop completed with undelivered notifications
error and stops.
About 18% of console errors filed against ResizeObserver in Sentry’s 2024 errors dataset are this loop error. Avoid by:
- Using the entry’s reported size, not measuring again.
- Mutating only properties whose change does not affect the
observed box (e.g.,
color, notpadding). - Debouncing inside the callback when the mutation must affect layout.
Where it differs from IntersectionObserver
| Concern | IntersectionObserver | ResizeObserver |
|---|---|---|
| Reports on | Visibility changes | Size changes |
| Fires before paint | Async (microtask after layout) | Yes (microtask before paint) |
| Multiple thresholds | Yes | No (one event per change) |
Use both: IntersectionObserver for “is the element on screen?”
and ResizeObserver for “what size is it?”.
Cross-engine support
ResizeObserver reached interop in 2020 across Chromium 64
(2018), WebKit 13.1 (2020), and Gecko 69 (2019). The
box: 'device-pixel-content-box' value reached interop in 2022.
caniuse for ResizeObserver reports about 99% global support. Pass-rate on the WPT subset is about 98% across all three engines.
Common patterns
Container-query-style component variants
const ro = new ResizeObserver(([entry]) => {
const w = entry.contentBoxSize[0].inlineSize;
entry.target.dataset.width =
w < 480 ? 'narrow' : w < 800 ? 'medium' : 'wide';
});
ro.observe(document.querySelector('.card'));
CSS reads the data-width attribute and adjusts layout.
Container queries
(/css/layout-primitives) are now the
preferred CSS-only solution; ResizeObserver remains the right
choice when the variant logic is JS-driven (e.g., switches a
chart between bar and line at the same threshold).
Auto-resize a <textarea>
const ro = new ResizeObserver(([entry]) => {
entry.target.style.blockSize = `${entry.target.scrollHeight}px`;
});
document.querySelectorAll('textarea').forEach((t) => ro.observe(t));
Watches the textarea’s box; when content overflows, the resize
fires, the script reads scrollHeight, and resets the height —
all without a keydown listener.
Disconnect on dispose
class WikiCard extends HTMLElement {
#ro = new ResizeObserver(this.#onResize);
connectedCallback() { this.#ro.observe(this); }
disconnectedCallback() { this.#ro.disconnect(); }
}
Without disconnect(), the observer holds a reference to the
element and prevents garbage collection.
Common pitfalls
- Reading the live size in the callback. Use
entry.contentBoxSize; callinggetBoundingClientRect()forces an extra layout. - No throttle on heavy work. A scroll-induced resize storm
fires the callback at frame rate. Wrap heavy work in
requestIdleCallback. - Multiple observers on the same element. Allowed, but causes redundant callbacks. Prefer one observer with multiple observed elements.
Further reading
- W3C Resize Observer.
- Surma’s ResizeObserver: it’s like
document.onresizefor elements — canonical intro. - The Sentry post-mortem on the loop error is the densest practical reference.