← Back to DevBytes

Implementing ResizeObserver API in Modern Web Applications

Understanding ResizeObserver

The ResizeObserver API provides a performant, event-driven mechanism for detecting changes to the dimensions of DOM elements. Unlike the older approach of polling element sizes with setInterval or relying on the global window.resize event, ResizeObserver delivers granular notifications directly to a callback function whenever a targeted element's content rectangle changes size.

At its core, ResizeObserver watches one or more elements and fires a callback with an array of ResizeObserverEntry objects. Each entry contains the element that changed, its new dimensions via contentRect, and more detailed sizing information through contentBoxSize, borderBoxSize, and devicePixelContentBoxSize properties. This granularity makes it invaluable for building responsive components, dynamic layouts, and adaptive user interfaces.

Key properties of ResizeObserverEntry

Why ResizeObserver Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Before ResizeObserver, developers had to resort to inefficient workarounds to track element resizes. The window.resize event only fires when the viewport changes, not when individual elements resize due to content changes, CSS flex/grid layout shifts, or dynamic DOM manipulation. Hacks like requestAnimationFrame polling loops or MutationObserver combined with computed style checks consumed CPU cycles even when nothing changed. ResizeObserver solves these problems elegantly.

Common use cases

Basic Implementation

Creating a ResizeObserver involves three steps: instantiate the observer with a callback, call observe() on target elements, and handle cleanup with unobserve() or disconnect(). The callback receives an array of entries and the observer instance itself, giving you access to every element that changed during that frame.

Simple observation example

// Create the observer with a callback
const observer = new ResizeObserver((entries, observerInstance) => {
  entries.forEach(entry => {
    const element = entry.target;
    const rect = entry.contentRect;
    
    console.log(`${element.id || element.tagName} resized:`);
    console.log(`  Width: ${rect.width}px, Height: ${rect.height}px`);
    
    // Access border box size for layout calculations
    const borderBox = entry.borderBoxSize[0];
    console.log(`  Border box: ${borderBox.inlineSize} x ${borderBox.blockSize}`);
  });
});

// Start observing a DOM element
const targetElement = document.querySelector('.responsive-panel');
observer.observe(targetElement);

// You can observe multiple elements
const sidebar = document.querySelector('.sidebar');
observer.observe(sidebar);

Handling the callback data

The callback fires immediately after construction if any observed element has a non-zero size. This "initial trigger" behavior ensures you can set up your component's state based on current dimensions without waiting for an actual resize. Always handle this initial callback gracefully.

const resizeObserver = new ResizeObserver(entries => {
  // The callback may fire immediately with initial dimensions
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    
    // Guard against zero dimensions during element initialization
    if (width === 0 && height === 0) {
      console.log('Element has zero dimensions, deferring setup');
      return;
    }
    
    // Proceed with dimension-dependent logic
    updateLayout(entry.target, width, height);
  }
});

function updateLayout(element, width, height) {
  // Example: switch between compact and expanded layout
  if (width < 400) {
    element.classList.add('compact-mode');
    element.classList.remove('expanded-mode');
  } else {
    element.classList.add('expanded-mode');
    element.classList.remove('compact-mode');
  }
}

Practical Examples

Example 1: Responsive Canvas Renderer

A common challenge is keeping a canvas element's internal resolution synchronized with its CSS layout size. ResizeObserver makes this trivial by notifying you whenever the container changes, allowing you to update the canvas buffer size and redraw.

<div class="canvas-container">
  <canvas id="drawing-canvas"></canvas>
</div>

<script>
const canvas = document.getElementById('drawing-canvas');
const ctx = canvas.getContext('2d');
const container = canvas.parentElement;

// Observe the container, not the canvas itself
const canvasObserver = new ResizeObserver(entries => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    
    // Match canvas buffer to CSS size for crisp rendering
    // Use devicePixelContentBoxSize for high-DPI displays
    const deviceSize = entry.devicePixelContentBoxSize?.[0] 
      ?? { inlineSize: width * window.devicePixelRatio, 
           blockSize: height * window.devicePixelRatio };
    
    canvas.width = deviceSize.inlineSize;
    canvas.height = deviceSize.blockSize;
    
    // Scale context to account for device pixel ratio
    ctx.setTransform(1, 0, 0, 1, 0, 0);
    ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
    
    // Redraw your content
    drawScene(width, height);
  }
});

canvasObserver.observe(container);

function drawScene(logicalWidth, logicalHeight) {
  ctx.clearRect(0, 0, logicalWidth, logicalHeight);
  // Draw rectangles, paths, images scaled to logical dimensions
  ctx.fillStyle = '#2196F3';
  ctx.fillRect(10, 10, logicalWidth - 20, logicalHeight - 20);
  ctx.fillStyle = '#FFFFFF';
  ctx.font = `${Math.min(logicalWidth / 10, 24)}px sans-serif`;
  ctx.textAlign = 'center';
  ctx.fillText('Canvas resized!', logicalWidth / 2, logicalHeight / 2);
}
</script>

Example 2: Component-Level Media Queries

Traditional CSS media queries only consider the viewport. With ResizeObserver, you can implement container queries that adapt a component based on its own width. This is the foundation of modern responsive component design and pairs well with CSS container queries for styling.

<style>
  .widget {
    transition: all 0.3s ease;
    padding: 16px;
    border-radius: 8px;
  }
  .widget.compact {
    background: #e3f2fd;
    font-size: 12px;
    flex-direction: column;
  }
  .widget.medium {
    background: #bbdefb;
    font-size: 16px;
    flex-direction: row;
  }
  .widget.expanded {
    background: #90caf9;
    font-size: 20px;
    flex-direction: row;
    gap: 24px;
  }
  .widget .detail-panel {
    display: none;
  }
  .widget.expanded .detail-panel {
    display: block;
  }
</style>

<div class="widget" id="adaptive-widget">
  <div class="summary">Summary content always visible</div>
  <div class="detail-panel">Extra details for wide layouts</div>
</div>

<script>
const widget = document.getElementById('adaptive-widget');

const componentObserver = new ResizeObserver(entries => {
  for (const entry of entries) {
    const width = entry.contentRect.width;
    const element = entry.target;
    
    // Remove all size classes first
    element.classList.remove('compact', 'medium', 'expanded');
    
    // Apply the appropriate class based on component width
    if (width < 300) {
      element.classList.add('compact');
    } else if (width < 600) {
      element.classList.add('medium');
    } else {
      element.classList.add('expanded');
    }
    
    // Log current breakpoint
    console.log(`Widget width: ${width}px → ${element.classList[1]} mode`);
  }
});

componentObserver.observe(widget);
</script>

Example 3: Synchronizing Multiple Elements

ResizeObserver can watch many elements simultaneously. This is useful for coordinating interdependent components, such as keeping a floating panel aligned with a resizing reference element or maintaining equal heights across a row of cards.

<div class="card-row">
  <div class="card" id="card-1">Card 1 - This has variable content that may wrap</div>
  <div class="card" id="card-2">Card 2 - Shorter</div>
  <div class="card" id="card-3">Card 3 - Medium length content for demonstration</div>
</div>

<script>
const cards = document.querySelectorAll('.card');

const syncObserver = new ResizeObserver(entries => {
  // Track maximum height across all observed cards
  let maxHeight = 0;
  const heights = new Map();
  
  // First pass: collect all heights
  for (const entry of entries) {
    const height = entry.contentRect.height;
    heights.set(entry.target, height);
    if (height > maxHeight) {
      maxHeight = height;
    }
  }
  
  // Second pass: apply max height to all cards
  // Only update elements that need it to avoid layout thrashing
  heights.forEach((currentHeight, element) => {
    if (currentHeight < maxHeight) {
      element.style.minHeight = `${maxHeight}px`;
    } else {
      element.style.minHeight = ''; // Reset if it's already tallest
    }
  });
  
  console.log(`Synced ${heights.size} cards to max height: ${maxHeight}px`);
});

// Observe all cards
cards.forEach(card => syncObserver.observe(card));
</script>

Example 4: Detecting Text Overflow

ResizeObserver helps detect when text content overflows its container. By comparing the scrollHeight to the contentRect height, you can determine if text is truncated and apply tooltips or expandable UI patterns.

<style>
  .truncated-text {
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    padding: 8px 12px;
    border: 1px solid #ccc;
    border-radius: 4px;
    max-width: 100%;
    transition: border-color 0.2s;
  }
  .truncated-text.overflowing {
    border-color: #ff9800;
    cursor: pointer;
  }
</style>

<div class="truncated-text" id="text-block">
  This is a very long piece of text that might overflow depending on the container width
</div>

<script>
const textBlock = document.getElementById('text-block');

const overflowObserver = new ResizeObserver(entries => {
  for (const entry of entries) {
    const element = entry.target;
    
    // Compare scroll dimensions to content rect
    // scrollWidth/scrollHeight includes overflowing content
    const isOverflowing = 
      element.scrollWidth > entry.contentRect.width ||
      element.scrollHeight > entry.contentRect.height;
    
    if (isOverflowing) {
      element.classList.add('overflowing');
      element.title = element.textContent; // Show full text as tooltip
    } else {
      element.classList.remove('overflowing');
      element.title = '';
    }
  }
});

overflowObserver.observe(textBlock);

// Add click handler to expand overflowing text
textBlock.addEventListener('click', function() {
  if (this.classList.contains('overflowing')) {
    this.style.whiteSpace = 'normal';
    this.style.overflow = 'visible';
    this.classList.remove('overflowing');
  }
});
</script>

Best Practices

1. Observe the correct element

Think carefully about which element to observe. For a canvas, observe its container rather than the canvas itself, because the canvas element's internal size changes programmatically. For layout-driven adaptations, observe the element whose dimensions dictate the layout change. If you need to track an element that is itself being resized by its children, observe that parent directly.

// GOOD: Observe the container that drives canvas size
observer.observe(canvas.parentElement);

// LESS IDEAL: Observing the canvas itself may cause infinite loops
// if your callback modifies canvas dimensions

2. Guard against zero-size entries

The callback may fire with zero dimensions during element initialization, when elements are detached from the DOM, or during display:none transitions. Always check for meaningful dimensions before proceeding with layout calculations.

const observer = new ResizeObserver(entries => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    if (width === 0 && height === 0) {
      // Element is hidden or not yet laid out
      return;
    }
    // Safe to proceed with dimension-dependent logic
    handleResize(entry.target, width, height);
  }
});

3. Avoid infinite resize loops

If your callback modifies the DOM in a way that changes the size of an observed element, you risk creating an infinite resize loop. The browser will throw a ResizeObserver loop limit exceeded error if too many consecutive callbacks fire. To prevent this, either observe a different element whose size isn't affected by your callback, or batch DOM updates and use requestAnimationFrame.

let pendingUpdate = false;

const safeObserver = new ResizeObserver(entries => {
  if (pendingUpdate) return; // Skip if already scheduled
  
  pendingUpdate = true;
  requestAnimationFrame(() => {
    for (const entry of entries) {
      // Perform DOM updates that might affect sizes
      updateComponentLayout(entry.target, entry.contentRect.width);
    }
    pendingUpdate = false;
  });
});

4. Clean up observers

Always call disconnect() when an observer is no longer needed, such as during component teardown or SPA route transitions. For individual elements, use unobserve() to stop watching them while keeping the observer active for others. Memory leaks from abandoned observers are common in long-running applications.

class ResizableComponent {
  constructor(element) {
    this.element = element;
    this.observer = new ResizeObserver(this.handleResize.bind(this));
    this.observer.observe(element);
  }
  
  handleResize(entries) {
    // Handle resize logic
  }
  
  destroy() {
    // Clean up: stop observing and release reference
    this.observer.disconnect();
    this.observer = null;
    this.element = null;
  }
}

// For partial cleanup:
observer.unobserve(specificElement);

5. Use the appropriate sizing property

Choose the right sizing property for your use case. contentRect is the most straightforward for general purposes. borderBoxSize accounts for borders and padding, which is useful for layout calculations. devicePixelContentBoxSize gives physical pixels for canvas and WebGL. contentBoxSize provides writing-mode aware inline and block sizes.

const observer = new ResizeObserver(entries => {
  for (const entry of entries) {
    // For most UI work, contentRect is sufficient
    const cssWidth = entry.contentRect.width;
    
    // For canvas/WebGL, use device pixel size
    const [deviceSize] = entry.devicePixelContentBoxSize;
    const physicalWidth = deviceSize.inlineSize;
    
    // For layout calculations including borders/padding
    const [borderSize] = entry.borderBoxSize;
    const totalWidth = borderSize.inlineSize;
  }
});

6. Debounce rapid resize events

While ResizeObserver batches updates into a single callback per frame, complex layout transitions can fire multiple callbacks across consecutive frames. For expensive operations like API calls or complex recalculations, implement a debounce strategy.

function createDebouncedObserver(callback, delay = 150) {
  let timerId = null;
  let latestEntries = [];
  
  const observer = new ResizeObserver(entries => {
    // Accumulate entries from multiple frames
    latestEntries = [...latestEntries, ...entries];
    
    if (timerId !== null) {
      clearTimeout(timerId);
    }
    
    timerId = setTimeout(() => {
      callback(latestEntries);
      latestEntries = [];
      timerId = null;
    }, delay);
  });
  
  return observer;
}

// Usage: expensive chart recalculation
const debouncedObserver = createDebouncedObserver(entries => {
  console.log(`Debounced: processing ${entries.length} entries`);
  for (const entry of entries) {
    recalculateExpensiveChart(entry.target, entry.contentRect.width);
  }
}, 200);

debouncedObserver.observe(document.querySelector('.chart-container'));

7. Combine with MutationObserver for comprehensive monitoring

ResizeObserver detects size changes but not content changes that don't affect dimensions. For components that need to react to both content and size changes, pair ResizeObserver with MutationObserver.

function createComprehensiveObserver(element, onContentChange, onResize) {
  // Watch for DOM mutations that might affect content
  const mutationObserver = new MutationObserver(mutations => {
    const hasRelevantChanges = mutations.some(m => 
      m.type === 'childList' || m.type === 'characterData'
    );
    if (hasRelevantChanges) {
      onContentChange(mutations);
    }
  });
  
  mutationObserver.observe(element, {
    childList: true,
    subtree: true,
    characterData: true
  });
  
  // Watch for size changes
  const resizeObserver = new ResizeObserver(entries => {
    onResize(entries);
  });
  
  resizeObserver.observe(element);
  
  // Return cleanup function
  return () => {
    mutationObserver.disconnect();
    resizeObserver.disconnect();
  };
}

// Usage
const cleanup = createComprehensiveObserver(
  document.querySelector('.dynamic-panel'),
  mutations => console.log('Content changed', mutations),
  entries => console.log('Size changed', entries)
);

// Later: cleanup();

Browser Support and Polyfills

ResizeObserver is supported in all modern browsers, including Chrome 64+, Firefox 69+, Safari 13.1+, and Edge 79+. For older browsers, several polyfills exist that fall back to a requestAnimationFrame polling loop combined with a cloned "scroll listener" technique using hidden scrollable elements. The most widely used polyfill is @juggle/resize-observer, which you can import conditionally.

// Feature detection with polyfill loading
async function ensureResizeObserver() {
  if (typeof ResizeObserver === 'undefined') {
    // Dynamic import of polyfill
    const { ResizeObserver: PolyfillRO } = await import('@juggle/resize-observer');
    window.ResizeObserver = PolyfillRO;
  }
  return window.ResizeObserver;
}

// Usage in an async context
(async () => {
  const RO = await ensureResizeObserver();
  const observer = new RO(entries => {
    console.log('Resize detected via native or polyfill');
  });
  observer.observe(document.querySelector('.legacy-component'));
})();

Conclusion

ResizeObserver has transformed how developers handle element dimension changes in modern web applications. By providing an efficient, event-driven API that fires precisely when elements resize, it eliminates the need for polling hacks and delivers superior performance. Whether you're building responsive canvas renderers, component-level media queries, synchronized layouts, or overflow detection systems, ResizeObserver gives you the tools to create adaptive, performant interfaces. Remember to observe the correct element, guard against zero-size entries, prevent infinite loops, clean up observers properly, and choose the appropriate sizing property for your use case. With these best practices in mind, you can leverage ResizeObserver to build robust, dimension-aware components that respond gracefully to the dynamic nature of the web.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles