The ResizeObserver API: A Practical Guide for Frontend Developers

Reacting to an element changing size used to mean polling with setInterval or wiring up a brittle window.resize listener and hoping for the best. ResizeObserver is the browser-native fix — it fires a callback the moment any element changes size, for any reason, with the new dimensions handed to you directly.

⚡ Quick Answer

ResizeObserver is a JavaScript API that lets you watch any DOM element and run a callback whenever that element’s size changes — not just when the window resizes, but for any reason: a parent flexbox reflowing, content being added, a CSS class toggle, or a user dragging a resize handle. You create one observer, call .observe(element) on any elements you care about, and read the new dimensions from the callback’s entry object.


📋 Table of Contents

  1. Why ResizeObserver Exists
  2. Basic Syntax — The Three Lines You Need
  3. Reading the Entry Object
  4. borderBoxSize vs contentBoxSize — Which One to Use?
  5. inlineSize and blockSize — What Those Names Actually Mean
  6. Live Demo — Shape-Shifting Boxes
  7. Observing Multiple Elements with One Observer
  8. Cleanup: unobserve() and disconnect()
  9. Pitfalls to Know
  10. Browser Support
  11. ResizeObserver vs window.resize — Quick Comparison
  12. FAQ

Why ResizeObserver Exists

The old way of reacting to layout changes was window.addEventListener('resize', handler). That fires when the browser window resizes — but an element can change size for dozens of other reasons that have nothing to do with the window: a sidebar collapsing, a parent container going from flex-row to flex-column, text being dynamically inserted, a CSS animation changing a height, a user dragging a resize handle on a <textarea>.

Before ResizeObserver, the only alternatives were either hacks (a hidden <iframe> that fired resize events, looping getBoundingClientRect() calls in setInterval) or accepting that you simply couldn’t know. ResizeObserver is the browser saying: here’s a proper, efficient, callback-based API for exactly this problem.

🧒 Explain it like I’m new to this

Imagine you’re a window blind that needs to adjust every time the window frame changes size. window.resize is someone tapping you on the shoulder only when the outer wall of the building moves. ResizeObserver is a sensor attached directly to your frame — it fires the moment your frame moves, regardless of what caused it.


Basic Syntax — The Three Lines You Need

There are just three things you ever do with a ResizeObservercreate it with a callback, observe an element, and optionally stop watching later.

// 1. Create an observer with a callback
const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    console.log('element resized:', entry.target);
  }
});

// 2. Start watching an element
observer.observe(document.getElementById('my-element'));

// 3. Stop watching when done (e.g. component unmounts)
observer.unobserve(document.getElementById('my-element'));
ℹ️
The callback receives an array of entries, not a single entry — because multiple observed elements can resize in the same frame, and the browser delivers them all at once. Always use a for...of loop, even if you’re only watching one element.

Reading the Entry Object

Each entry in the callback array is a ResizeObserverEntry object. These are the properties you’ll use:

PropertyWhat it gives you
entry.targetA reference to the actual DOM element that changed size. Use this to figure out which element fired when observing multiple elements with one observer.
entry.borderBoxSize[0]An object with inlineSize and blockSize — the size of the element including padding and border.
entry.contentBoxSize[0]An object with inlineSize and blockSize — the size of the element’s inner content area, excluding padding and border.
entry.contentRectA legacy DOMRectReadOnly with familiar width and height properties. Easier to read, but considered a legacy API — prefer borderBoxSize or contentBoxSize in new code.
Why is it [0] — why an array?
Each size property is an array to support multi-column fragmented elements, where one element can span multiple layout fragments each with different sizes. For standard single-column content, [0] is always the one you want. This also means the old Firefox pattern of accessing entry.contentBoxSize.inlineSize directly (without the index) throws a TypeError in modern browsers — always use [0].

borderBoxSize vs contentBoxSize — Which One to Use?

The difference maps directly to the CSS box model. Here’s a visual:

📦 CSS Box Model
borderBoxSize — includes everything below
padding
The actual content area

Use borderBoxSize when you want to know the total space the element occupies on screen — the number that matches what you’d see from getBoundingClientRect(). This is the right choice for most layout-reaction logic, including the demo in this tutorial.

Use contentBoxSize when you care about the space available for the element’s children specifically — for example, deciding how many columns to show inside a container without accounting for its own border.


inlineSize and blockSize — What Those Names Actually Mean

If you’re used to width and height, these names look strange at first. They’re writing-mode-aware equivalents:

PropertyStandard LTR layoutVertical writing mode
inlineSizeWidth (horizontal)Height (vertical)
blockSizeHeight (vertical)Width (horizontal)
🧒 Explain it like I’m new to this

“Inline” means “along the direction text flows” — left-to-right for English, so it maps to width. “Block” means “perpendicular to text flow” — top-to-bottom for English, so it maps to height. In Japanese vertical writing mode, those would swap. Using inlineSize/blockSize means your code stays correct regardless of which direction text flows — unlike hardcoding width and height, which are always physical.

For the vast majority of English-language sites with horizontal text, you can safely read inlineSize as width and blockSize as height.


Live Demo — Shape-Shifting Boxes

This is the demo from your code — two boxes that turn into circles when you resize them below 150×150px. Drag the bottom-right corner of either box to resize it. The shape change is driven entirely by ResizeObserver reading borderBoxSize.

Live demo — drag the corner of either box to resize

↙ Drag the bottom-right corner of each box. Shrink below 150×150 to trigger the circle shape.

A 200 × 200
B 200 × 200

Each box independently tracks its own size. The shape changes when blockSize < 150 and inlineSize < 150 — exactly as written in the code below.

Here is the exact code powering this demo:

<div id="flex">
  <div class="box box-1">A</div>
  <div class="box box-2">B</div>
</div>
#flex {
  display: flex;
  gap: 12px;
}

.box {
  border: 1px solid #ccc;
  width: 300px;
  height: 300px;
  display: flex;
  align-items: center;
  justify-content: center;
  resize: both;      /* lets the user drag-resize */
  overflow: hidden;  /* required for `resize: both` to work */
}

.box-1 { background-color: beige; }
.box-2 { background-color: aqua;  }
const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const target = entry.target;
    const box    = entry.borderBoxSize[0];

    if (box.blockSize < 150 && box.inlineSize < 150) {
      target.style.borderRadius = '100%';
    } else {
      target.style.borderRadius = 'unset';
    }
  }
});

const boxes = document.querySelectorAll('.box');
boxes.forEach((b) => {
  observer.observe(b);
});

Notice what this code doesn’t need: no scroll listener, no polling, no window.resize. Each box independently triggers the callback the moment its own size crosses the 150px threshold in both dimensions. Both boxes are watched by a single observer, and entry.target tells the callback which box triggered each time.


Observing Multiple Elements with One Observer

One ResizeObserver can watch as many elements as you need. Calling .observe() again on the same observer simply adds another element to the watch list — it doesn’t replace the existing ones.

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    // entry.target tells you which element changed
    console.log(entry.target.id, entry.borderBoxSize[0]);
  }
});

observer.observe(document.getElementById('sidebar'));
observer.observe(document.getElementById('main-content'));
observer.observe(document.getElementById('footer'));
// All three reported in the same callback batch
💡
One observer is more efficient than many. The browser batches all size-change notifications for a single observer and delivers them together. Using one observer for ten elements is faster than ten observers for one element each — and as you saw in the demo code above, you only need entry.target to tell them apart inside the callback.

Cleanup: unobserve() and disconnect()

Always stop observing elements when you no longer need them — especially elements that are removed from the DOM. An observer holding a reference to a detached node is a memory leak.

// Stop watching one specific element
observer.unobserve(element);

// Stop watching ALL elements and destroy the observer entirely
observer.disconnect();
MethodWhen to use it
unobserve(el)You’re removing or hiding one specific element but keeping the observer active for others.
disconnect()You’re done with the observer entirely — component unmounts, page navigates, SPA route changes.

Pitfalls to Know

The resize loop warning

If your callback sets a style on the element being observed that causes it to resize again, the browser catches this infinite loop and logs: “ResizeObserver loop completed with undelivered notifications.” It won’t break your page, but it means your callback is fighting itself.

// ❌ This causes a resize loop — the callback changes the element's width,
//    which fires the observer again, which changes the width again...
const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    entry.target.style.width =
      entry.borderBoxSize[0].inlineSize + 10 + 'px';
  }
});
⚠️
Rule of thumb: never write back a dimensional change (width, height, padding, font-size) to the element you’re observing. Changing visual properties that don’t affect layout — like border-radius, color, or opacity — is perfectly safe and is what the demo above does.

The callback fires immediately on observe()

As soon as you call observer.observe(element), the callback fires once with the element’s current size — even if nothing has changed yet. This is intentional and useful (you get the initial size for free), but it means your callback will run on page load, not just on later resize events. Make sure your callback logic is safe to run with whatever the initial size is.

It only works on the main thread

ResizeObserver is a Window-only API. You cannot create one inside a Web Worker. If you need size data in a worker, measure it on the main thread and postMessage the dimensions across.


Browser Support

ResizeObserver has excellent support across all modern browsers — around 96% global coverage as of 2026. Internet Explorer never supported it.

BrowserResizeObserverborderBoxSize / contentBoxSize
Chrome / Edge✓ Chrome 64+, Edge 79+✓ Chrome 84+
Firefox✓ Firefox 69+✓ Firefox 92+ (as array)
Safari✓ Safari 13.1+ (macOS), 13.4+ (iOS)✓ Safari 15.4+
Internet Explorer✗ Never supported✗ Never supported
ℹ️
If you still need to support older environments, the safest fallback is reading entry.contentRect.width and entry.contentRect.height — the legacy contentRect property has broader support than the newer box-size properties, and is still in the spec for exactly this reason.

ResizeObserver vs window.resize — Quick Comparison

window.resizeResizeObserver
What it watchesThe entire browser viewportAny specific element(s)
Triggers on viewport resize?✓ Yes✓ Yes (if the element’s size changes)
Triggers on parent layout change?✗ No✓ Yes
Triggers on content change?✗ No✓ Yes
Gives you element dimensions✗ No (gives you window size)✓ Yes (directly on each entry)
Multiple targets✗ Only window✓ Unlimited elements

Frequently Asked Questions

What is the ResizeObserver API?

ResizeObserver is a browser API that lets you run a JavaScript callback whenever a specific DOM element changes size — not just when the browser window resizes, but when any element changes for any reason: a parent layout shifting, content being added, a CSS class toggling, or a user dragging a resize handle.

What is the difference between inlineSize and blockSize?

inlineSize is the size along the direction text flows — horizontal width in a standard left-to-right layout. blockSize is perpendicular to that — vertical height in a standard layout. These names swap correctly in vertical or RTL writing modes, unlike physical width and height.

What is the difference between borderBoxSize and contentBoxSize?

borderBoxSize includes the element’s content, padding, and border — the total space it occupies. contentBoxSize measures only the inner content area, excluding padding and border. For most layout-reaction logic, borderBoxSize matches what CSS already calculated for the element’s visual size.

Should I use one ResizeObserver per element, or one for everything?

One observer can watch as many elements as you need — just call observe() on each. A single observer is more efficient because the browser batches and delivers all callbacks together. Use entry.target inside the callback to tell which element changed.

What is the ResizeObserver loop error?

The “ResizeObserver loop completed with undelivered notifications” warning fires when your callback changes the observed element’s size, which triggers another callback, creating an infinite loop. Avoid writing any dimensional change back to the element being observed — changing non-layout properties like border-radius is safe.


Summary

ResizeObserver gives you a clean, efficient way to react to any element changing size — something window.resize was never designed to handle.

  • Create one observer with a callback, call .observe(el) for every element you care about
  • Read entry.target to identify which element changed, and entry.borderBoxSize[0] for its new size
  • Use inlineSize for width and blockSize for height in standard horizontal layouts
  • Always unobserve() before removing a DOM element to avoid memory leaks
  • Never write a dimensional property back to the element you’re observing — that causes the resize loop warning
  • The callback fires once immediately on observe() — this is intentional and useful

Posted

in

,

by

Advertisement