skip to content

ResizeObserver

You will learn to observe element size changes — not just window resizes — and the loop error that catches everyone who writes to layout inside the callback. Interviewers ask this for responsive components and chart sizing.

on this pageshow

questions

5

A sidebar can be dragged wider by the user and also shrinks when a neighbouring panel opens, while the browser window stays the same size. Why is a window 'resize' event listener not enough to react to that element's own size changing, and what does ResizeObserver give you instead?

level: juniorimportance: must knowfreq 72%

answer

  1. viewport size versus element box
  2. the window never changed size
  3. the browser measures, you receive
  4. one callback per frame, entries array

basics

~20 s

The window resize event fires only when the viewport changes size. An element's own box can change from dragging, sibling layout, content, or CSS while the viewport never moves. ResizeObserver watches a chosen element and reports its new dimensions.

solid answer

~40 s

The `resize` event on `window` reports one thing: the viewport (or frame) changed size. An element's box changes for many other reasons — a flex sibling collapsing, content growing, a web font swapping in, a user dragging a splitter, a width transition — and none of those fire a window event, so a resize listener simply never runs. The old workaround was to listen for viewport resizes and re-measure every candidate element with `getBoundingClientRect()`, which both missed those cases and made you read layout by hand. `ResizeObserver` inverts it: `const ro = new ResizeObserver(callback); ro.observe(el)`. The callback receives an array of `ResizeObserverEntry` objects, each with a `target` and its already-measured sizes, batched once per frame before paint. You call `ro.disconnect()` when the component goes away.

code

javascript · 14 lines
javascript
const sidebar = document.createElement('div');
sidebar.style.width = '300px';
document.body.append(sidebar);

const ro = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { inlineSize } = entry.contentBoxSize[0];
    entry.target.dataset.layout = inlineSize < 480 ? 'narrow' : 'wide';
  }
});
ro.observe(sidebar);

// on teardown:
// ro.disconnect();

go deeper

for a junior

Know that resize on window is about the viewport only, and that ResizeObserver watches a specific element. Be able to write the three lines: construct with a callback, call observe(el), call disconnect() when done.

for a middle

Explain the entry shape — target plus already-measured sizes — and that delivery is batched once per frame inside the rendering update, which is why manual debouncing is usually pointless.

for a senior

Show judgment about what the API does not cover: position, transforms, and unrendered elements. Say how you would guard a callback that can receive zero sizes and how teardown is wired into the component lifecycle so observers do not accumulate.

for a principal

Frame it as a boundary decision: which size-dependent behaviour belongs in JavaScript at all, and what it costs to have dozens of components each subscribing to their own layout feedback loop.

## Two different things can change size The viewport and any given element change size independently. The `resize` event fires on `window` when the viewport — the browser window or the containing frame — changes dimensions. That is the whole of its contract. It says nothing about which elements were affected, and it does not fire at all when an element resizes for a reason internal to the page. And there are many such reasons. A flex or grid sibling appears or collapses, so the remaining tracks are re-sized. Content grows: a longer label, an image finishing decode, a list gaining rows. A web font swaps in and text reflows. The user drags a splitter or resizes a `<textarea>`. A CSS transition animates `width`. A scrollbar appears and steals a dozen pixels. The element is moved into a different container. In every one of those cases the viewport is untouched, so a `resize` listener never runs. ## The old workaround, and why it was bad Before `ResizeObserver`, the pattern was: listen for `resize` on `window`, debounce it, then loop over the elements you care about calling `getBoundingClientRect()` or reading `offsetWidth`. Sometimes a `setInterval` poll was bolted on to catch the non-viewport cases. This is wrong twice over — it misses every element-level cause of a size change, and it makes your code responsible for reading layout at a moment of its own choosing rather than a moment the browser has chosen. ## What ResizeObserver does The API is small: ```javascript const ro = new ResizeObserver((entries, observer) => { /* ... */ }); ro.observe(element); ``` One observer can watch many elements. Each invocation of the callback receives an array of `ResizeObserverEntry` objects — one per element whose observed box changed since the last delivery. Each entry carries: - `target` — the element, so a shared observer can dispatch on it; - `contentRect` — a `DOMRectReadOnly` whose `width`/`height` are the content box; - `contentBoxSize`, `borderBoxSize`, `devicePixelContentBoxSize` — the modern size descriptions. The key point for a first answer: the browser has already measured. You do not call `getBoundingClientRect()` inside the callback; the numbers are handed to you. ## When the callback runs ResizeObserver delivery is part of the browser's rendering update for a frame. Layout has been computed, `requestAnimationFrame` callbacks have already run, and the frame has not yet been painted. Three consequences follow. First, the sizes you receive are current for this frame. Second, a style you write from the callback can still affect the frame the user sees — which is convenient, and is also the origin of the well-known ResizeObserver loop error when your write changes the size you are observing. Third, delivery is batched: fifty size changes in one frame produce one callback with the changed entries, not fifty callbacks. You do not need to debounce it yourself; the frame is the natural throttle. ## What it will not tell you - **Movement.** ResizeObserver is about size, not position. An element pushed 200px down the page with an unchanged box produces no notification. - **Transforms.** `transform: scale(2)` changes how the element is painted, not its layout box, so nothing fires. If you need the painted rectangle, `getBoundingClientRect()` is still the tool. - **Scrolling.** Scrolling changes neither the viewport size nor the element's box. - **Unrendered elements.** An element with `display: none` has no box at all. ## Teardown The observer is a live subscription. Call `ro.unobserve(element)` to stop watching one element, or `ro.disconnect()` to stop watching all of them, in whatever teardown path your component has. Do not leave it to garbage collection — an observer that is still observing will still run your callback, for elements that may no longer be on screen.

  • If ResizeObserver batches per frame, do you still need to debounce the callback?
    Usually not. Delivery already happens at most once per frame per observer, with all changed entries in one array, so the callback cannot run faster than the display refresh. Debounce only if the work inside the callback is genuinely expensive — a network request, a heavy re-render — and even then prefer coalescing that work rather than delaying the measurement you were given.
  • You need to know when an element moves on screen, not when it resizes. Does ResizeObserver help?
    No. ResizeObserver only reports box size changes, so an element pushed down the page by a sibling produces nothing. If you need the current on-screen rectangle you have to read `getBoundingClientRect()` at a moment you choose — typically inside a `requestAnimationFrame` callback — and accept that there is no event telling you the position changed.
  • Does ResizeObserver fire when the observed element is hidden with display: none?
    An element with `display: none` generates no box, so there is no content or border box to report; implementations deliver a zero-sized entry at the transition and then stay silent while it is hidden. Guard your callback against zero sizes rather than treating every entry as a meaningful new layout.

A window resize listener is a fire alarm for the whole building; ResizeObserver is a sensor you stick on one door frame.

saying these in an interview costs you the question

  • Says the window resize event fires when any element changes size
  • Believes transform: scale() triggers a ResizeObserver notification
  • Claims ResizeObserver reports when an element moves on screen
  • Calls getBoundingClientRect inside the callback for sizes already provided
  • Adds a setInterval poll as a fallback for element resizes

context

open as a page

When you call observe(el) on a ResizeObserver, when does the callback first run, and what do unobserve() and disconnect() each do when the code that created the observer is torn down?

level: middleimportance: must knowfreq 58%

basics

~20 s

Observing delivers an initial callback at the next rendering opportunity with the element's current size, even though nothing changed. unobserve(target) stops watching one element; disconnect() stops watching all of them and is what teardown should call, since the observer stays live otherwise.

open as a page

In a ResizeObserver callback, how do an entry's contentRect, contentBoxSize, borderBoxSize and devicePixelContentBoxSize differ, and what does the box option passed to observe() actually control?

level: middleimportance: should knowfreq 45%

basics

~20 s

contentRect is the legacy content-box rectangle; contentBoxSize, borderBoxSize and devicePixelContentBoxSize are arrays of {inlineSize, blockSize} for the content box, the border box, and the content box in device pixels. The box option chooses which box's changes trigger a notification, not which properties you get.

open as a page

A page logs 'ResizeObserver loop completed with undelivered notifications' to the console, and the CI test runner fails the build because it treats it as an uncaught error. What produces that message, and how would you fix the code causing it?

level: seniorimportance: should knowfreq 50%

basics

~20 s

A ResizeObserver callback changed something that resized an observed element in the same frame, so the browser re-ran layout and re-delivered repeatedly. When it cannot settle, it stops, defers the remaining notifications to the next frame, and fires an error event on window. Fix the feedback, don't silence it.

open as a page

A design system has thirty components, and each one constructs its own ResizeObserver when it mounts. As the lead, how would you decide between per-instance observers and a single shared observer with an element-to-handler registry, and what does each option cost?

level: principalimportance: nice to knowfreq 28%

basics

~20 s

Per-instance observers keep ownership and failure local and are the right default. A shared observer with an element-to-handler registry cuts object count and gives one callback per frame plus one place to enforce policy, at the cost of a singleton whose registry can leak and whose callback is a shared failure point.

open as a page