ResizeObserver in JavaScript: Track Element Size Changes Without Polling

tutorialApril 27, 2026ยท 5 min read

Every time you've built a chart, a virtualised list, or a component that reacts to its own size, you've probably wrestled with this: how do I know when an element's dimensions change?

The naive approaches are painful:

  • window.resize โ€” fires on window size changes, not your element's
  • Polling offsetWidth/Height โ€” janky, runs on the main thread, bad for performance
  • MutationObserver on parent โ€” fires on any DOM change, expensive, misses CSS changes

ResizeObserver solves this directly. It fires when an element's content or padding size changes โ€” and only then. No polling, no noise.

The Core API

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    console.log('Element resized:', entry.target);
    console.log('New size:', entry.contentRect);
  }
});

observer.observe(document.querySelector('.my-element'));

entries is an array because you can observe multiple elements with one observer. Each entry has:

interface ResizeObserverEntry {
  target: Element;           // which element changed
  contentRect: DOMRectReadOnly; // the new content box dimensions
  borderBoxSize: ResizeObserverSize; // border box (older spec)
  contentBoxSize: ResizeObserverSize; // content box (newer spec)
  devicePixelContentBoxSize: ResizeObserverSize; // device pixels
}

contentRect is the most commonly used:

interface DOMRectReadOnly {
  x: number;      // left edge position relative to layout root
  y: number;      // top edge position relative to layout root
  width: number;  // content width
  height: number; // content height
  top: number;    // distance from layout root top to top edge
  right: number;
  bottom: number;
  left: number;
}

A Practical Example: Responsive Typography

A common use case โ€” adjust font size based on the container width:

function setupFluidTypography(element) {
  new ResizeObserver(([entry]) => {
    const { width } = entry.contentRect;
    // Scale from 16px at 320px wide to 24px at 640px wide
    const fontSize = Math.max(16, Math.min(24, (width / 640) * 24 + 16));
    element.style.fontSize = `${fontSize}px`;
  }).observe(element);
}

This is cleaner than window.resize because it only reacts to the element you care about.

The Aspect Ratio Lock Pattern

If you've ever wanted an element to maintain a specific aspect ratio as it resizes โ€” think video embeds, image cards, or hero sections โ€” ResizeObserver makes it clean:

function lockAspectRatio(element, ratio = 16 / 9) {
  new ResizeObserver(([entry]) => {
    const { width } = entry.contentRect;
    element.style.height = `${width / ratio}px`;
  }).observe(element);
}

// Usage
lockAspectRatio(document.querySelector('.video-embed'));

This is better than the old padding-bottom hack because:

  1. It works with any height property (flex children, grid items)
  2. It fires on actual layout changes, not just initial render
  3. Multiple elements can have different ratios independently

Chart Libraries: The Real Use Case

ResizeObserver is how chart libraries like Chart.js and Recharts handle responsive resizing. Here's a simplified version of what they do:

class ResponsiveChart {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.observer = new ResizeObserver(this.handleResize.bind(this));
    this.observer.observe(canvas.parentElement);
  }

  handleResize(entries) {
    const [{ contentRect }] = entries;
    const { width, height } = contentRect;

    // Set canvas buffer size to match display size
    const dpr = window.devicePixelRatio || 1;
    this.canvas.width = width * dpr;
    this.canvas.height = height * dpr;
    this.canvas.style.width = `${width}px`;
    this.canvas.style.height = `${height}px`;

    // Scale the drawing context so our chart code stays in CSS pixels
    this.ctx.scale(dpr, dpr);
    this.draw();
  }

  draw() {
    // Chart drawing logic using this.canvas.width/height
  }
}

The key insight: ResizeObserver watches the parent, not the canvas itself, because CSS can resize any ancestor and affect the canvas indirectly.

Virtualised Lists: Size-Aware Rendering

Virtual list libraries (react-virtual, react-window) use ResizeObserver to detect when the container size changes and recalculate how many items to render:

class VirtualScroller {
  constructor(container, items, itemHeight) {
    this.container = container;
    this.items = items;
    this.itemHeight = itemHeight;
    this.rowHeight = 50; // estimated, can be dynamic

    this.observer = new ResizeObserver(this.onResize.bind(this));
    this.observer.observe(container);
  }

  onResize([{ contentRect }]) {
    const visibleCount = Math.ceil(contentRect.height / this.rowHeight);
    const scrollTop = this.container.scrollTop;

    this.startIndex = Math.floor(scrollTop / this.rowHeight);
    this.endIndex = Math.min(
      this.startIndex + visibleCount + 2, // +2 for buffer
      this.items.length
    );

    this.render();
  }

  render() {
    // Only render items in [startIndex, endIndex)
    // Position them absolutely using transform: translateY()
  }
}

The devicePixelContentBoxSize โ€” For Canvas and WebGL

When drawing on a <canvas> or WebGL context, you need to account for device pixel ratio to avoid blurry rendering. The devicePixelContentBoxSize array gives you dimensions in actual device pixels:

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    // For a HiDPI/Retina display, this gives physical pixels
    const [{ inlineSize, blockSize }] = entry.devicePixelContentBoxSize;

    canvas.width = inlineSize;
    canvas.height = blockSize;

    // Draw in physical pixels โ€” context is already scaled
    drawScene();
  }
});

observer.observe(canvas, { box: 'device-pixel-content-box' });

Note: devicePixelContentBoxSize is newer and may not be supported in all browsers โ€” always polyfill with contentRect fallback:

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    let width, height;

    if (entry.devicePixelContentBoxSize) {
      // Newer browsers: physical pixels
      [{ inlineSize: width, blockSize: height }] = entry.devicePixelContentBoxSize;
    } else {
      // Fallback: CSS pixels
      width = entry.contentRect.width;
      height = entry.contentRect.height;
    }

    // ...
  }
});

The box Option โ€” Which Box to Watch

ResizeObserver can watch different box models:

// Watch content box (default) โ€” the inside of padding box
observer.observe(element, { box: 'content-box' });

// Watch border box โ€” the outer edge of the border
observer.observe(element, { box: 'border-box' });

// Watch device pixel content box (for canvas/WebGL)
observer.observe(element, { box: 'device-pixel-content-box' });

In practice, content-box (the default) is what you want 90% of the time.

Disconnect and Unobserve

Don't forget to clean up:

class MyComponent {
  constructor() {
    this.observer = new ResizeObserver(this.onResize.bind(this));
    this.observer.observe(this.el);
  }

  destroy() {
    // Stop observing everything
    this.observer.disconnect();
  }
}

If you only want to stop observing a specific element (while keeping the observer active for others):

observer.unobserve(specificElement);

Common Pitfalls

ResizeObserver is asynchronous

ResizeObserver callbacks fire after layout, but not synchronously โ€” they're batched. This is good for performance (you won't get mid-layout snapshots) but means you can't use it to prevent a layout.

Content box vs border box confusion

The contentRect reports the content box (inside padding), not the border box. If you have a thick border and expect contentRect.width to include it โ€” it won't.

.my-element {
  width: 200px;
  padding: 20px;
  border: 10px solid red;
}

contentRect.width will be 200 โ€” the border and padding are outside it.

The element must be in the DOM

ResizeObserver only fires when the observed element is in the layout tree. If you observe a hidden element (display: none or visibility: hidden), it won't fire.

Comparison with Alternatives

ApproachWhat it detectsPerformanceNotes
ResizeObserverElement's own content boxโœ… ExcellentUse this
window.resizeBrowser viewport onlyโš ๏ธ Triggers on anythingUse for full-page layouts
Polling offsetWidthAny elementโŒ BadCauses layout thrashing
MutationObserverDOM children changesโš ๏ธ Misses CSS-only changesUse for DOM structure, not size
IntersectionObserverElement visibilityโœ… GoodDifferent use case โ€” visibility, not size

Summary

  • ResizeObserver fires when an element's content box changes
  • It's more precise than window.resize and more efficient than polling
  • Use contentRect for CSS pixel dimensions, devicePixelContentBoxSize for HiDPI canvases
  • Clean up with disconnect() or unobserve() when done
  • Libraries like Chart.js, react-virtual, and Vue's v-resize directive all use it under the hood

If you've been polling window.innerWidth or watching a parent MutationObserver to detect layout changes โ€” this is the tool you're looking for.


ยฉ 2024, Built with Gatsby