SalakCode SalakCode
Browser APIs

Resize Observer

React to element size changes

Intermediate
browser api responsive performance

Definition

ResizeObserver reports changes to the dimensions of an Element’s content or border box, or the bounding box of an SVGElement. It provides a performant way to respond to element size changes without polling or expensive getBoundingClientRect() calls.

Basic Usage

Creating an Observer

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    console.log('Element resized:', entry.target);
    console.log('New size:', entry.contentRect);
    
    const { width, height, top, left, bottom, right } = entry.contentRect;
    
    // Respond to size change
    handleResize(entry.target, width, height);
  }
});

// Observe elements
const elements = document.querySelectorAll('.resizable');
elements.forEach(el => observer.observe(el));

// Stop observing
// observer.unobserve(element);
// observer.disconnect();

ResizeObserverEntry

{
  target: Element,           // The resized element
  contentRect: DOMRectReadOnly, {
    width: number,
    height: number,
    x: number,  // left
    y: number,  // top
    top: number,
    right: number,
    bottom: number,
    left: number
  },
  borderBoxSize: [{ inlineSize, blockSize }],
  contentBoxSize: [{ inlineSize, blockSize }],
  devicePixelContentBoxSize: [{ inlineSize, blockSize }]
}

Practical Use Cases

Responsive Canvas

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');

const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;
    
    // Set canvas size to match container
    canvas.width = width * window.devicePixelRatio;
    canvas.height = height * window.devicePixelRatio;
    
    // Scale context for high DPI displays
    ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
    
    // Redraw content
    drawScene(width, height);
  }
});

resizeObserver.observe(canvas.parentElement);

Text Fit

function fitText(element) {
  const resizeObserver = new ResizeObserver((entries) => {
    for (const entry of entries) {
      const { width } = entry.contentRect;
      const textLength = element.textContent.length;
      
      // Calculate optimal font size
      const fontSize = Math.min(width / (textLength * 0.6), 48);
      element.style.fontSize = `${fontSize}px`;
    }
  });
  
  resizeObserver.observe(element);
  return () => resizeObserver.disconnect();
}

// Usage
fitText(document.querySelector('.hero-title'));

Responsive Grid Layout

const grid = document.querySelector('.responsive-grid');

const resizeObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const width = entry.contentRect.width;
    
    // Adjust columns based on container width
    if (width < 600) {
      grid.style.gridTemplateColumns = '1fr';
    } else if (width < 900) {
      grid.style.gridTemplateColumns = 'repeat(2, 1fr)';
    } else {
      grid.style.gridTemplateColumns = 'repeat(3, 1fr)';
    }
  }
});

resizeObserver.observe(grid);

Box Options

Content Box vs Border Box

// Observe content box (default)
observer.observe(element);
// or
observer.observe(element, { box: 'content-box' });

// Observe border box (includes padding and border)
observer.observe(element, { box: 'border-box' });

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

Handling Different Boxes

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    // Content box size
    const contentWidth = entry.contentRect.width;
    const contentHeight = entry.contentRect.height;
    
    // Border box size (newer API)
    if (entry.borderBoxSize) {
      const borderWidth = entry.borderBoxSize[0].inlineSize;
      const borderHeight = entry.borderBoxSize[0].blockSize;
    }
  }
});

Advanced Patterns

Debounced Resize Handler

function createDebouncedResizeObserver(callback, delay = 100) {
  let timeoutId;
  
  return new ResizeObserver((entries) => {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      callback(entries);
    }, delay);
  });
}

// Usage
const observer = createDebouncedResizeObserver((entries) => {
  // Runs at most every 100ms
  expensiveLayoutCalculation(entries);
}, 100);

Breakpoint-Based Classes

function responsiveClassManager(element, breakpoints) {
  const observer = new ResizeObserver((entries) => {
    for (const entry of entries) {
      const width = entry.contentRect.width;
      
      // Remove all breakpoint classes
      breakpoints.forEach(bp => {
        element.classList.remove(bp.class);
      });
      
      // Add appropriate class
      const activeBreakpoint = breakpoints
        .slice()
        .reverse()
        .find(bp => width >= bp.minWidth);
      
      if (activeBreakpoint) {
        element.classList.add(activeBreakpoint.class);
      }
    }
  });
  
  observer.observe(element);
  return () => observer.disconnect();
}

// Usage
responsiveClassManager(container, [
  { minWidth: 0, class: 'size-xs' },
  { minWidth: 576, class: 'size-sm' },
  { minWidth: 768, class: 'size-md' },
  { minWidth: 992, class: 'size-lg' },
  { minWidth: 1200, class: 'size-xl' }
]);

Component Library Pattern

class ResizableComponent extends HTMLElement {
  constructor() {
    super();
    this.resizeObserver = null;
  }
  
  connectedCallback() {
    this.resizeObserver = new ResizeObserver((entries) => {
      for (const entry of entries) {
        this.handleResize(entry.contentRect);
      }
    });
    
    this.resizeObserver.observe(this);
  }
  
  disconnectedCallback() {
    this.resizeObserver?.disconnect();
  }
  
  handleResize({ width, height }) {
    // Override in subclasses
  }
}

Performance Best Practices

Batch Processing

const observer = new ResizeObserver((entries) => {
  // Process all entries in single frame
  requestAnimationFrame(() => {
    entries.forEach(entry => {
      updateElement(entry.target, entry.contentRect);
    });
  });
});

Avoid Layout Thrashing

// Bad: Reading and writing interleaved
const observer = new ResizeObserver((entries) => {
  entries.forEach(entry => {
    const height = entry.target.clientHeight; // Read
    entry.target.style.height = height + 10 + 'px'; // Write
    const newHeight = entry.target.clientHeight; // Read again!
  });
});

// Good: Batch reads and writes
const observer = new ResizeObserver((entries) => {
  // Collect measurements
  const measurements = entries.map(entry => ({
    target: entry.target,
    width: entry.contentRect.width
  }));
  
  // Apply all changes in next frame
  requestAnimationFrame(() => {
    measurements.forEach(({ target, width }) => {
      target.style.fontSize = width / 20 + 'px';
    });
  });
});

Comparison with Other Methods

// Old way: Window resize event (coarse)
window.addEventListener('resize', () => {
  // Fires for any window resize, not element-specific
  const rect = element.getBoundingClientRect();
  handleResize(rect.width, rect.height);
});

// Old way: Polling (inefficient)
setInterval(() => {
  const rect = element.getBoundingClientRect();
  if (rect.width !== lastWidth) {
    handleResize(rect.width, rect.height);
  }
}, 100);

// New way: ResizeObserver (precise and efficient)
const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    handleResize(entry.contentRect.width, entry.contentRect.height);
  }
});
observer.observe(element);

Browser Support

// Feature detection
if ('ResizeObserver' in window) {
  // Use ResizeObserver
} else {
  // Fallback to ResizeObserver polyfill
  // or window resize + getBoundingClientRect
}

// Polyfill
import { ResizeObserver } from '@juggle/resize-observer';
Key Takeaway

ResizeObserver is the modern solution for responding to element size changes. It replaces polling and window resize workarounds with an efficient, element-specific API. Use it for responsive canvas, text fitting, and container query-like behaviors. Always disconnect observers when components unmount.

Resources

Related Topics