How to use the Resize Observer API in JavaScript
How the Resize Observer API works
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
The content-box default works well for most cases because it tracks the space available for child elements. Switching to border-box is useful when you need to account for padding and borders in your calculations. The code below shows how to read the border box dimensions instead:
// 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" });
With the box model option understood, let’s apply the observer to a real UI pattern: making a card component switch between compact and expanded layouts depending on how much horizontal space it actually has.
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));
The class-toggle approach works for simple cases, but real components often need more granular control. Instead of binary compact/expanded states, you can define named breakpoints and dispatch custom events whenever the element crosses a threshold. This decouples the size detection from the layout response.
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 }
}));
}
});
});
Breakpoints work well for CSS-driven layout changes. For canvas-based rendering, Resize Observer solves a different problem: redrawing at the correct resolution. Charts and visualizations need both the container’s CSS dimensions and the device pixel ratio to avoid blurry output on high-DPI screens.
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();
}
}
The chart example creates one observer per component, which is clean and predictable. When you need to track several elements that share layout logic—like a sidebar, main content area, and footer—a single observer can handle all of them at once. The callback receives one entry per observed element, so you can coordinate layout adjustments across multiple regions from one place.
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.
So far the examples have focused on container resizing from external causes like window changes. Resize Observer also triggers when internal content grows or shrinks, such as text being dynamically inserted or removed. That makes it valuable for components like expandable text blocks, chat messages, and accordion panels.
Handling content changes that cause resize
Resize Observer also fires when content inside an element changes its size, which is 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
}
The content change example uses contentRect which returns CSS pixels. For canvas rendering and image processing on high-DPI screens, you need the actual physical pixel count. The device-pixel-content-box option provides that directly, so you can write sharp output without manually multiplying by devicePixelRatio.
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.
Every observer allocates resources internally, so knowing when to stop observing is just as important as knowing how to start. In single-page applications, failing to clean up observers leads to memory leaks and callbacks firing on removed DOM elements.
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.
Choosing resize observer well
Resize Observer is most useful when layout depends on the size of a specific element rather than the whole window. That is an important distinction. Responsive pages often care about viewport width, but components care about the space they actually have. A sidebar, card, canvas, or editor panel can change size for reasons that have nothing to do with the window itself. Resize Observer gives you a direct signal for those cases, which makes the code feel much closer to the problem.
The API is also a good match for situations where the content itself can change the layout. A message block may grow after data loads, or a widget may collapse when text becomes shorter. Watching the element directly keeps the logic honest because you react to the real size, not an assumption about what the size should be. That often leads to less fragile code than checking the viewport and guessing how the component might respond.
Avoiding layout thrash
A resize callback should stay focused on state updates, not on expensive measurement loops. If you keep reading and writing layout in a tight cycle, the browser may have to recalculate styles more often than necessary. The better pattern is to read the size, decide what should change, and then apply the smallest update that fits the new shape. That keeps the code easier to reason about and helps the browser do less work.
It is also a good idea to choose one source of truth for the current size. If several parts of the app are all trying to infer the same layout breakpoints, the code can drift apart. A single observer that updates a shared state value or data attribute is often easier to maintain. That way, the component owns its own size logic and the rest of the app can react to the result instead of repeating the measurement.
Cleanup And Scope
Observers should have a clear lifetime. If a component mounts an observer, it should also disconnect it when the component disappears. That prevents stale callbacks and keeps memory use under control in longer-running pages. The same habit applies when observing several elements: know which part of the app owns the observer and when it should stop watching. A clear owner makes the code much easier to debug later.
Final resize note
Resize Observer is easiest to keep healthy when one component owns the size logic for one piece of UI. That keeps the callback small and the response clear. If several areas of the app all need the same measurement, it is usually better to centralize the value once and let the rest of the UI react to it instead of measuring again in multiple places.
That pattern makes the callback easier to reason about and keeps the browser from doing extra work that does not change the result.
It also gives the component a cleaner ownership story, which matters when the page grows and more code wants the same measurement.
That clarity makes later changes easier to fit in without reworking the whole layout flow.
Next steps
Once you have Resize Observer working, combine it with Intersection Observer for components that need both visibility and size tracking. Canvas-heavy apps benefit from reading devicePixelContentBoxSize instead of the standard content rect, since that accounts for the actual physical pixels on high-DPI screens. For SPAs, make sure every observer has a paired disconnect() call in the component’s teardown path to avoid memory leaks.
See Also
- MDN: Resize Observer API, official documentation
- The Intersection Observer API, detect when elements enter or leave the viewport
- Working with the DOM, DOM manipulation basics