skip to content

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