JavaScript Resize Observer API
Learn the JavaScript Resize Observer API to react to size changes of individual elements — for responsive components and auto-growing inputs.
The ResizeObserver API lets you react when a specific element changes size, no matter why it changed. It belongs to the same family as the MutationObserver (which watches the DOM tree) and the IntersectionObserver (which watches visibility): efficient, callback-driven observers that replace clumsy polling loops.
Why Not Just Use the resize Event?
The window resize event only fires when the browser window changes size. But an element on the page can change size for many other reasons:
- A CSS rule changes (a media query kicks in, a class is toggled).
- Its content changes (text is loaded, an image arrives, rows are added to a list).
- A flexbox or grid layout reflows because a sibling grew or shrank.
- A parent container is resized by a draggable splitter, a sidebar, or a panel.
None of these necessarily resize the window, so window.addEventListener('resize', ...) never fires. You could poll with setInterval and compare getBoundingClientRect() every few milliseconds, but that wastes CPU and still lags behind the real change.
ResizeObserver solves this: it watches one or more elements and calls you back only when their size actually changes.
ResizeObserver reports an element's content size by default, not its position. If you need to know when an element moves or scrolls into view, reach for IntersectionObserver instead. For viewport-level measurements, see window sizes and scrolling.
Basic Usage
The pattern mirrors the other observers: create an observer with a callback, then tell it which element(s) to observe.
const box = document.querySelector('#box');
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
console.log(`${entry.target.id} is now ${width} x ${height}`);
}
});
ro.observe(box);The callback receives an array of entries — one per observed element that changed in this batch — so a single observer can watch many elements at once.
What's Inside an Entry
Each entry describes one element and its new size. There are two ways to read that size.
The easy way is entry.contentRect, a DOMRectReadOnly with width, height, top, and left (the offsets are relative to the element's padding box):
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
console.log(entry.target); // the element
console.log(entry.contentRect.width); // content-box width in px
console.log(entry.contentRect.height); // content-box height in px
}
});The more precise way uses the box-size properties, which are arrays of { inlineSize, blockSize } objects (an array because an element can be fragmented across columns):
entry.contentBoxSize— the content box, excluding padding and border.entry.borderBoxSize— the border box, including padding and border.entry.devicePixelContentBoxSize— the content box measured in device pixels, ideal for sharp canvas rendering on high-DPI screens.
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
// inlineSize ~ width, blockSize ~ height (for a horizontal writing mode)
const { inlineSize, blockSize } = entry.borderBoxSize[0];
console.log(`border box: ${inlineSize} x ${blockSize}`);
}
});By default the observer reacts to changes in the content box. To observe the border box instead, pass an options object:
ro.observe(box, { box: 'border-box' });inlineSize and blockSize are writing-mode aware. In the default left-to-right, top-to-bottom mode, inlineSize is the width and blockSize is the height — but in a vertical writing mode they swap. Prefer them over hard-coded width/height when your UI must support multiple writing directions.
Methods
ResizeObserver exposes three methods:
observe(element, options)— start watching an element. Call it once per element. The optionaloptions.boxcan be'content-box'(default),'border-box', or'device-pixel-content-box'.unobserve(element)— stop watching a single element.disconnect()— stop watching all elements at once.
ro.observe(el); // start
ro.unobserve(el); // stop watching this one
ro.disconnect(); // stop watching everythingUse Case 1 — Responsive Components ("Container Queries in JS")
A media query reacts to the viewport. But a reusable card might be wide in the main column and narrow in a sidebar at the very same viewport width. With ResizeObserver you can adapt a component to its own width:
const card = document.querySelector('.card');
const ro = new ResizeObserver(([entry]) => {
const width = entry.contentRect.width;
// Toggle a layout class based on the element's own width.
card.classList.toggle('card--compact', width < 400);
});
ro.observe(card);.card { display: flex; gap: 1rem; }
.card--compact { flex-direction: column; }Now the card stacks vertically whenever it is narrower than 400px, regardless of the window size — handy in dashboards, split panes, and embeddable widgets.
Modern CSS can do this natively with container queries (@container). If you only need to restyle based on a container's size, prefer CSS container queries — they are declarative and run off the main thread. Reach for ResizeObserver when you need to run JavaScript in response to the size change (recompute values, redraw a canvas, reflow a chart).
Use Case 2 — Resizing a Canvas or Chart
A <canvas> has two sizes: its CSS display size and its drawing-buffer size (canvas.width/canvas.height). If they disagree, the bitmap is stretched and looks blurry. ResizeObserver keeps the buffer matched to the displayed size so drawings stay crisp:
const canvas = document.querySelector('#chart');
const ctx = canvas.getContext('2d');
const ro = new ResizeObserver(([entry]) => {
// Use device-pixel size for sharp rendering on high-DPI screens.
const size = entry.devicePixelContentBoxSize?.[0];
const width = size ? size.inlineSize : entry.contentRect.width;
const height = size ? size.blockSize : entry.contentRect.height;
canvas.width = width;
canvas.height = height;
redraw(ctx, width, height); // your drawing / charting code
});
ro.observe(canvas, { box: 'device-pixel-content-box' });The same idea applies to charting libraries: observe the chart's container and call the library's resize() method when the container changes, instead of hooking only into window.resize.
Use Case 3 — Auto-Growing a Textarea
Because the callback fires whenever the element's measured size changes, you can keep dependent elements in sync. A classic example is mirroring a textarea's height onto a sibling, or reacting to content-driven growth:
const textarea = document.querySelector('#message');
const counter = document.querySelector('#height-readout');
const ro = new ResizeObserver(([entry]) => {
const h = Math.round(entry.contentRect.height);
counter.textContent = `${h}px tall`;
});
ro.observe(textarea);Whenever the user drags the textarea's resize handle — or your code changes its height as the user types — the readout updates automatically, with no resize event and no polling.
The "ResizeObserver loop" Warning
You may run into this console message:
ResizeObserver loop completed with undelivered notifications.
(Some browsers phrase it as "ResizeObserver loop limit exceeded.") It happens when your callback changes the size of the observed element, which triggers the observer again, which changes the size again — a feedback loop.
// Anti-pattern: this can cause the loop warning.
const ro = new ResizeObserver(([entry]) => {
// Resizing the observed element from inside its own callback. Bad.
entry.target.style.height = entry.contentRect.width + 'px';
});Don't synchronously resize the observed element inside its own callback. If a size-driven resize is unavoidable, break the cycle: only write when the value actually changed (a guard), or defer the write with requestAnimationFrame. The warning is usually harmless and self-correcting, but a true infinite loop will hurt performance.
Cleaning Up
An observer keeps a reference to every element it watches, which can prevent that element from being garbage-collected. Always stop observing when you're done — for example, when a component unmounts or a view is torn down:
function mountWidget(el) {
const ro = new ResizeObserver(handleResize);
ro.observe(el);
// Return a cleanup function.
return () => ro.disconnect();
}Forgetting this is a common source of memory leaks in single-page apps. For more on keeping size-and-layout work cheap, see DOM performance optimization.
ResizeObserver is supported in all modern browsers, so you can rely on it without a polyfill in any current evergreen browser.