Here is something every frontend developer has written:
window.addEventListener('resize', () => {
const width = container.getBoundingClientRect().width;
updateLayout(width);
});
It works when the user drags the browser window. It does not work when the container changes size for any other reason: a parent flex layout shifts because sibling content loaded, a container query breakpoint triggers a CSS change, an image inside it loads and forces reflow, or you programmatically modify its children. window resize fires for one of the many things that can resize an element. ResizeObserver was built for the rest.
How ResizeObserver works
You pass a callback and tell it which elements to watch:
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
updateLayout(width, height);
}
});
observer.observe(container);
The callback fires whenever the observed element's size changes — regardless of why it changed. Stop watching with unobserve or disconnect:
observer.unobserve(container); // stop watching one element
observer.disconnect(); // stop watching all at once
The callback receives an array of entries because a single ResizeObserver can observe multiple elements, and the browser may batch several size changes into one notification. Always loop over all entries.
contentRect, contentBoxSize, and borderBoxSize
Each entry gives you three ways to read the new dimensions:
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
// Older property — still works, content area only
const { width, height } = entry.contentRect;
// Logical dimensions — inlineSize maps to width in horizontal writing modes
const { inlineSize, blockSize } = entry.contentBoxSize[0];
// Includes padding and border, like offsetWidth
const { inlineSize: borderWidth } = entry.borderBoxSize[0];
}
});
contentBoxSize and borderBoxSize use logical dimension names (inlineSize for width in LTR/RTL layouts, blockSize for height) so the same code works correctly in vertical writing modes. For standard horizontal layouts, contentBoxSize[0].inlineSize is the content width and borderBoxSize[0].inlineSize is what offsetWidth would return — without the layout read.
🎮 Try it yourself
▶️ Open the interactive playground →
Runs right in your browser — poke at it and watch the concept react live.
The canvas resize pattern
The classic use case: keeping a <canvas> pixel-perfect as its container resizes.
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { inlineSize: width, blockSize: height } = entry.contentBoxSize[0];
const dpr = window.devicePixelRatio ?? 1;
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
ctx.scale(dpr, dpr);
draw();
}
});
observer.observe(canvas);
With a window resize listener, you miss every canvas resize that happens without the viewport changing — panels collapsing, sidebars toggling, content loading. With ResizeObserver, the canvas always matches its container, at the right pixel density.
No debouncing needed
Unlike scroll listeners or window resize handlers, ResizeObserver does not fire on every animation frame. The browser batches and schedules callbacks after layout and before paint, at most once per frame. You do not need to debounce or throttle it:
// Don't do this — delays the response for no benefit
const observer = new ResizeObserver(debounce((entries) => { ... }, 100));
// The browser already handles the batching
const observer = new ResizeObserver((entries) => { ... });
The debounce would add latency without removing any work, because ResizeObserver already delivers at the right moment.
Avoiding the infinite-loop trap
If your callback changes the observed element's size, it triggers another callback, which changes the size again. The browser detects this and logs ResizeObserver loop completed with undelivered notifications. The fix: do not write layout-affecting styles back to the element being observed inside the callback. If you need to respond to an element's width by applying a class or attribute (the element query pattern), make sure those class changes don't affect the element's own box dimensions — or defer the write with requestAnimationFrame so it lands outside the current resize notification cycle.
Browser support
ResizeObserver is Baseline 2020: Chrome 64 (January 2018), Firefox 69 (September 2019), Safari 13.1 (March 2020). Every modern browser ships it. There is no equivalent in Node.js (no layout engine), but for anything running in a browser, no polyfill is needed.
🧠 Test yourself
Think it clicked? Take the 6-question quiz →
Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.
The takeaway
If your codebase has container.getBoundingClientRect() inside a window resize handler, it's solving the wrong problem with the wrong tool. window resize is a proxy for element resize that silently misses everything that doesn't involve the viewport. ResizeObserver watches the element. The callback fires when the element's box changes, and it hands you the new dimensions directly — no follow-up layout read required.
Thanks for reading! Let's stay connected:
- ⭐ GitHub — follow me and star the projects: github.com/parsajiravand
- 📸 Instagram — frontend best practices, daily: @bestpractice___
- 💼 LinkedIn — linkedin.com/in/parsa-jiravand
- ✉️ Email (work & contract inquiries): bestpractice2026@gmail.com
Top comments (0)