The Resize Observer API

· 5 min read · Updated March 16, 2026 · intermediate
javascript browser-api dom resize-observer responsive

The Resize Observer API provides a way to observe changes to element dimensions. Unlike older approaches that relied on polling getBoundingClientRect on a timer, Resize Observer notifies you immediately when an element resizes—whether from window resizing, content changes, CSS modifications, or viewport adjustments.

This API is particularly useful for building responsive components, implementing custom layout behaviors, and creating charts or visualizations that adapt to their container size.

Basic Usage

Create a ResizeObserver and pass a callback that fires whenever observed elements change size:

const observer = new ResizeObserver((entries) => {
  entries.forEach(entry => {
    console.log("Element resized:", entry.target);
    console.log("New dimensions:", entry.contentRect);
  });
});

observer.observe(document.querySelector(".resize-target"));

The callback receives an array of ResizeObserverEntry objects. Each entry contains:

  • entry.target — the element being observed
  • entry.contentRect — a DOMRectReadOnly with width, height, x, y, top, right, bottom, left
  • entry.borderBoxSize — array of border box sizes
  • entry.contentBoxSize — array of content box sizes
  • entry.devicePixelContentBoxSize — sizes in physical pixels

Understanding the ResizeObserverOptions

The second argument to ResizeObserver is optional and controls which box models to observe:

const observer = new ResizeObserver(callback, {
  box: "content-box" // default
});

Available box values:

  • content-box — observes the content area (default)
  • border-box — observes the border area
  • device-pixel-content-box — observes physical pixels for the content box
// Observe border box changes instead of content box
const observer = new ResizeObserver((entries) => {
  entries.forEach(entry => {
    const borderBox = entry.borderBoxSize[0];
    console.log("Border box: " + borderBox.inlineSize + "x" + borderBox.blockSize);
  });
}, { box: "border-box" });

Practical Example: Responsive Card Component

A common pattern is adjusting a card layout based on available width:

const cards = document.querySelectorAll(".card");

const observer = new ResizeObserver((entries) => {
  entries.forEach(entry => {
    const width = entry.contentRect.width;
    const card = entry.target;
    
    // Switch between single-column and multi-column layouts
    if (width < 400) {
      card.classList.add("layout-compact");
      card.classList.remove("layout-expanded");
    } else {
      card.classList.add("layout-expanded");
      card.classList.remove("layout-compact");
    }
  });
});

cards.forEach(card => observer.observe(card));

Detecting Size Breakpoints

Combine Resize Observer with breakpoints for precise control:

function getBreakpoint(width) {
  if (width < 480) return "mobile";
  if (width < 768) return "tablet";
  if (width < 1024) return "desktop";
  return "wide";
}

const observer = new ResizeObserver((entries) => {
  entries.forEach(entry => {
    const newBreakpoint = getBreakpoint(entry.contentRect.width);
    const currentBreakpoint = entry.target.dataset.breakpoint;
    
    if (newBreakpoint !== currentBreakpoint) {
      entry.target.dataset.breakpoint = newBreakpoint;
      // Trigger layout changes
      entry.target.dispatchEvent(new CustomEvent("breakpoint-change", {
        detail: { breakpoint: newBreakpoint }
      }));
    }
  });
});

Implementing a Responsive Chart

Resize Observer excels for charts and visualizations that must adapt to their container:

class ResponsiveChart {
  constructor(container) {
    this.container = container;
    this.canvas = container.querySelector("canvas");
    this.ctx = this.canvas.getContext("2d");
    
    this.observer = new ResizeObserver((entries) => {
      this.handleResize(entries[0]);
    });
    
    this.observer.observe(container);
    this.handleResize({ contentRect: container.getBoundingClientRect() });
  }
  
  handleResize(entry) {
    const width = entry.contentRect.width;
    const height = entry.contentRect.height;
    const dpr = window.devicePixelRatio || 1;
    
    // Scale canvas for high DPI displays
    this.canvas.width = width * dpr;
    this.canvas.height = height * dpr;
    this.canvas.style.width = width + "px";
    this.canvas.style.height = height + "px";
    this.ctx.scale(dpr, dpr);
    
    this.render();
  }
  
  render() {
    // Redraw chart at new size
    const w = this.canvas.width / (window.devicePixelRatio || 1);
    const h = this.canvas.height / (window.devicePixelRatio || 1);
    
    this.ctx.clearRect(0, 0, w, h);
    this.ctx.fillStyle = "#3498db";
    this.ctx.fillRect(10, 10, w - 20, h - 20);
  }
  
  destroy() {
    this.observer.disconnect();
  }
}

Observing Multiple Elements

A single ResizeObserver can track multiple elements efficiently:

const sidebar = document.querySelector(".sidebar");
const mainContent = document.querySelector(".main-content");
const footer = document.querySelector(".footer");

const layoutObserver = new ResizeObserver((entries) => {
  const sizes = {};
  entries.forEach(entry => {
    sizes[entry.target.className] = entry.contentRect.width;
  });
  
  // Adjust main content based on sidebar width
  if (sizes.sidebar > 300) {
    mainContent.style.marginLeft = sizes.sidebar + "px";
  }
});

layoutObserver.observe(sidebar);
layoutObserver.observe(mainContent);
layoutObserver.observe(footer);

This approach is more performant than creating separate observers because a single observer manages all element measurements.

Handling Content Changes That Cause Resize

Resize Observer also fires when content inside an element changes its size—useful for text-heavy components:

const expandable = document.querySelector(".expandable");
const textElement = expandable.querySelector(".text");

const observer = new ResizeObserver((entries) => {
  entries.forEach(entry => {
    const height = entry.contentRect.height;
    
    if (height > 200) {
      expandable.classList.add("truncated");
    } else {
      expandable.classList.remove("truncated");
    }
  });
});

observer.observe(textElement);

// When content updates
function updateContent(newText) {
  textElement.textContent = newText;
  // ResizeObserver callback fires automatically
}

Getting Device Pixel Ratio Information

For precise rendering on high-DPI displays, use devicePixelContentBoxSize:

const observer = new ResizeObserver((entries) => {
  entries.forEach(entry => {
    const devicePixelWidth = entry.devicePixelContentBoxSize[0].inlineSize;
    const devicePixelHeight = entry.devicePixelContentBoxSize[0].blockSize;
    
    console.log("Physical pixels: " + devicePixelWidth + "x" + devicePixelHeight);
  });
}, { box: "device-pixel-content-box" });

This gives you the actual pixel count needed for sharp rendering, accounting for the device pixel ratio.

Cleaning Up

Always disconnect observers when they are no longer needed, especially in single-page applications:

// When component unmounts
observer.disconnect();

// Or unobserve specific elements while continuing to observe others
observer.unobserve(someElement);

Browser Support

Resize Observer is supported in all modern browsers (Chrome 64+, Firefox 69+, Safari 14.1+, Edge 79+). For older browsers, no polyfill exists.

Common Pitfalls

Ignoring device pixel ratio

Always account for window.devicePixelRatio when sizing canvases, images, or custom elements, or they will appear blurry on retina displays.

Observing too many elements

A single observer can handle hundreds of elements, but if you are observing thousands, consider whether you really need all those measurements.

Forgetting to unobserve

In SPAs, failing to disconnect observers causes memory leaks since the callback keeps firing for removed elements.

See Also