# JavaScript ResizeObserver API Complete Guide
Master element size monitoring: responsive components, adaptive layouts, performance optimization, and practical applications
The ResizeObserver API provides the ability to monitor element size changes. This article covers its usage and practical applications.
Basic Concepts
Creating an Observer
// Create observer
const observer = new ResizeObserver((entries) => {
entries.forEach(entry => {
console.log('Element resized:', entry.target);
console.log('New size:', entry.contentRect.width, entry.contentRect.height);
});
});
// Start observing
const element = document.getElementById('target');
observer.observe(element);
// Stop observing specific element
observer.unobserve(element);
// Disconnect all observations
observer.disconnect();
ResizeObserverEntry
const observer = new ResizeObserver((entries) => {
entries.forEach(entry => {
// Observed element
console.log(entry.target);
// Content box size (excluding padding and border)
console.log(entry.contentRect);
// { x, y, width, height, top, right, bottom, left }
// Content box size (array form)
console.log(entry.contentBoxSize);
// [{ inlineSize, blockSize }]
// Border box size
console.log(entry.borderBoxSize);
// [{ inlineSize, blockSize }]
// Device pixel content box size
console.log(entry.devicePixelContentBoxSize);
// [{ inlineSize, blockSize }]
});
});
Observation Options
const observer = new ResizeObserver(callback);
// Default: observe content box
observer.observe(element);
// Specify box model to observe
observer.observe(element, { box: 'content-box' }); // Default
observer.observe(element, { box: 'border-box' });
observer.observe(element, { box: 'device-pixel-content-box' });
Practical Applications
Responsive Component
class ResponsiveComponent {
constructor(element) {
this.element = element;
this.breakpoints = {
small: 320,
medium: 640,
large: 1024
};
this.observer = new ResizeObserver((entries) => {
const entry = entries[0];
this.handleResize(entry.contentRect.width);
});
this.observer.observe(element);
}
handleResize(width) {
const element = this.element;
// Remove all size classes
element.classList.remove('size-small', 'size-medium', 'size-large');
// Add corresponding size class
if (width < this.breakpoints.small) {
element.classList.add('size-small');
} else if (width < this.breakpoints.medium) {
element.classList.add('size-medium');
} else {
element.classList.add('size-large');
}
// Dispatch custom event
element.dispatchEvent(new CustomEvent('componentresize', {
detail: { width, breakpoint: this.getCurrentBreakpoint(width) }
}));
}
getCurrentBreakpoint(width) {
if (width < this.breakpoints.small) return 'small';
if (width < this.breakpoints.medium) return 'medium';
return 'large';
}
setBreakpoints(breakpoints) {
this.breakpoints = { ...this.breakpoints, ...breakpoints };
}
destroy() {
this.observer.disconnect();
}
}
// Usage
const component = new ResponsiveComponent(
document.getElementById('myComponent')
);
// Listen for custom event
document.getElementById('myComponent').addEventListener('componentresize', (e) => {
console.log('Current breakpoint:', e.detail.breakpoint);
});
Auto-Fit Text
class AutoFitText {
constructor(element, options = {}) {
this.element = element;
this.options = {
minSize: 12,
maxSize: 100,
step: 1,
...options
};
this.originalText = element.textContent;
this.observer = new ResizeObserver(() => this.fitText());
this.observer.observe(element);
}
fitText() {
const element = this.element;
const containerWidth = element.clientWidth;
// Create measuring element
const measurer = document.createElement('span');
measurer.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;';
measurer.textContent = this.originalText;
document.body.appendChild(measurer);
let fontSize = this.options.maxSize;
// Binary search for appropriate font size
let min = this.options.minSize;
let max = this.options.maxSize;
while (max - min > this.options.step) {
fontSize = Math.floor((min + max) / 2);
measurer.style.fontSize = fontSize + 'px';
if (measurer.offsetWidth > containerWidth) {
max = fontSize;
} else {
min = fontSize;
}
}
document.body.removeChild(measurer);
element.style.fontSize = min + 'px';
}
destroy() {
this.observer.disconnect();
}
}
// Usage
const autoText = new AutoFitText(
document.getElementById('title'),
{ minSize: 16, maxSize: 72 }
);
Responsive Chart
class ResponsiveChart {
constructor(container, chartOptions = {}) {
this.container = container;
this.chartOptions = chartOptions;
this.chart = null;
this.observer = new ResizeObserver(
this.debounce(() => this.resize(), 100)
);
this.init();
this.observer.observe(container);
}
init() {
const { width, height } = this.container.getBoundingClientRect();
this.chart = {
width,
height,
render: () => this.render()
};
this.render();
}
resize() {
const { width, height } = this.container.getBoundingClientRect();
if (width !== this.chart.width || height !== this.chart.height) {
this.chart.width = width;
this.chart.height = height;
this.render();
}
}
render() {
const { width, height } = this.chart;
// Clear container
this.container.textContent = '';
// Create Canvas
const canvas = document.createElement('canvas');
canvas.width = width * window.devicePixelRatio;
canvas.height = height * window.devicePixelRatio;
canvas.style.width = width + 'px';
canvas.style.height = height + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
// Draw chart
this.drawChart(ctx, width, height);
this.container.appendChild(canvas);
}
drawChart(ctx, width, height) {
// Example: Draw simple bar chart
const data = this.chartOptions.data || [30, 50, 80, 60, 40];
const barWidth = (width - 40) / data.length - 10;
const maxValue = Math.max(...data);
ctx.fillStyle = '#4CAF50';
data.forEach((value, index) => {
const barHeight = (value / maxValue) * (height - 40);
const x = 20 + index * (barWidth + 10);
const y = height - 20 - barHeight;
ctx.fillRect(x, y, barWidth, barHeight);
});
}
debounce(fn, delay) {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn.apply(this, args), delay);
};
}
destroy() {
this.observer.disconnect();
}
}
// Usage
const chart = new ResponsiveChart(
document.getElementById('chartContainer'),
{ data: [25, 40, 65, 55, 80, 45] }
);
Virtual Scrolling
class VirtualScroller {
constructor(container, options = {}) {
this.container = container;
this.options = {
itemHeight: 50,
buffer: 5,
items: [],
renderItem: (item) => item.toString(),
...options
};
this.visibleItems = [];
this.startIndex = 0;
this.endIndex = 0;
this.setupDOM();
this.setupObserver();
this.setupScrollListener();
this.render();
}
setupDOM() {
this.container.style.overflow = 'auto';
this.container.style.position = 'relative';
// Create content container
this.content = document.createElement('div');
this.content.style.position = 'relative';
this.updateContentHeight();
// Create viewport container
this.viewport = document.createElement('div');
this.viewport.style.position = 'absolute';
this.viewport.style.top = '0';
this.viewport.style.left = '0';
this.viewport.style.width = '100%';
this.content.appendChild(this.viewport);
this.container.appendChild(this.content);
}
setupObserver() {
this.observer = new ResizeObserver((entries) => {
this.containerHeight = entries[0].contentRect.height;
this.render();
});
this.observer.observe(this.container);
}
setupScrollListener() {
this.container.addEventListener('scroll', () => {
this.render();
});
}
updateContentHeight() {
const totalHeight = this.options.items.length * this.options.itemHeight;
this.content.style.height = totalHeight + 'px';
}
render() {
const scrollTop = this.container.scrollTop;
const { itemHeight, buffer, items, renderItem } = this.options;
// Calculate visible range
this.startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - buffer);
this.endIndex = Math.min(
items.length,
Math.ceil((scrollTop + this.containerHeight) / itemHeight) + buffer
);
// Update viewport position
this.viewport.style.transform = 'translateY(' + (this.startIndex * itemHeight) + 'px)';
// Render visible items
this.viewport.textContent = '';
for (let i = this.startIndex; i < this.endIndex; i++) {
const itemElement = document.createElement('div');
itemElement.style.height = itemHeight + 'px';
itemElement.style.boxSizing = 'border-box';
itemElement.textContent = renderItem(items[i], i);
this.viewport.appendChild(itemElement);
}
}
setItems(items) {
this.options.items = items;
this.updateContentHeight();
this.render();
}
scrollToIndex(index) {
this.container.scrollTop = index * this.options.itemHeight;
}
destroy() {
this.observer.disconnect();
}
}
// Usage
const items = Array.from({ length: 10000 }, (_, i) => 'Item ' + (i + 1));
const scroller = new VirtualScroller(
document.getElementById('scrollContainer'),
{
itemHeight: 40,
items,
renderItem: (item, index) => item + ' (Index: ' + index + ')'
}
);
Container Query Alternative
class ContainerQuery {
constructor(element, queries) {
this.element = element;
this.queries = queries;
this.observer = new ResizeObserver((entries) => {
const entry = entries[0];
this.evaluate(entry.contentRect);
});
this.observer.observe(element);
}
evaluate(rect) {
const { width, height } = rect;
Object.entries(this.queries).forEach(([name, condition]) => {
const matches = this.testCondition(condition, width, height);
this.element.classList.toggle(name, matches);
});
}
testCondition(condition, width, height) {
if (typeof condition === 'function') {
return condition(width, height);
}
const { minWidth, maxWidth, minHeight, maxHeight } = condition;
if (minWidth !== undefined && width < minWidth) return false;
if (maxWidth !== undefined && width > maxWidth) return false;
if (minHeight !== undefined && height < minHeight) return false;
if (maxHeight !== undefined && height > maxHeight) return false;
return true;
}
destroy() {
this.observer.disconnect();
}
}
// Usage
const containerQuery = new ContainerQuery(
document.getElementById('card'),
{
'card-small': { maxWidth: 300 },
'card-medium': { minWidth: 301, maxWidth: 600 },
'card-large': { minWidth: 601 },
'card-square': (w, h) => Math.abs(w - h) < 50
}
);
Layout Change Detection
class LayoutObserver {
constructor(elements, callback) {
this.elements = elements;
this.callback = callback;
this.sizes = new Map();
this.observer = new ResizeObserver((entries) => {
const changes = [];
entries.forEach(entry => {
const element = entry.target;
const oldSize = this.sizes.get(element);
const newSize = {
width: entry.contentRect.width,
height: entry.contentRect.height
};
if (oldSize) {
if (oldSize.width !== newSize.width || oldSize.height !== newSize.height) {
changes.push({
element,
oldSize,
newSize,
delta: {
width: newSize.width - oldSize.width,
height: newSize.height - oldSize.height
}
});
}
}
this.sizes.set(element, newSize);
});
if (changes.length > 0) {
this.callback(changes);
}
});
elements.forEach(el => {
this.observer.observe(el);
});
}
add(element) {
this.elements.push(element);
this.observer.observe(element);
}
remove(element) {
const index = this.elements.indexOf(element);
if (index > -1) {
this.elements.splice(index, 1);
this.observer.unobserve(element);
this.sizes.delete(element);
}
}
destroy() {
this.observer.disconnect();
this.sizes.clear();
}
}
// Usage
const elements = document.querySelectorAll('.resizable');
const layoutObserver = new LayoutObserver(
Array.from(elements),
(changes) => {
changes.forEach(change => {
console.log('Element size changed:',
change.delta.width.toFixed(0) + 'px',
change.delta.height.toFixed(0) + 'px'
);
});
}
);
Performance Optimization
Throttled Processing
class ThrottledResizeObserver {
constructor(callback, delay = 100) {
this.callback = callback;
this.delay = delay;
this.pending = false;
this.lastEntries = [];
this.observer = new ResizeObserver((entries) => {
this.lastEntries = entries;
if (!this.pending) {
this.pending = true;
requestAnimationFrame(() => {
this.callback(this.lastEntries);
this.pending = false;
});
}
});
}
observe(target, options) {
this.observer.observe(target, options);
}
unobserve(target) {
this.observer.unobserve(target);
}
disconnect() {
this.observer.disconnect();
}
}
// Usage
const throttledObserver = new ThrottledResizeObserver((entries) => {
entries.forEach(entry => {
console.log('Throttled resize:', entry.contentRect.width);
});
});
throttledObserver.observe(document.getElementById('target'));
Conditional Triggering
class ConditionalResizeObserver {
constructor(callback, condition) {
this.callback = callback;
this.condition = condition;
this.lastSizes = new Map();
this.observer = new ResizeObserver((entries) => {
const filtered = entries.filter(entry => {
const lastSize = this.lastSizes.get(entry.target);
const newSize = entry.contentRect;
const shouldTrigger = this.condition(newSize, lastSize, entry.target);
this.lastSizes.set(entry.target, {
width: newSize.width,
height: newSize.height
});
return shouldTrigger;
});
if (filtered.length > 0) {
this.callback(filtered);
}
});
}
observe(target, options) {
this.observer.observe(target, options);
}
disconnect() {
this.observer.disconnect();
this.lastSizes.clear();
}
}
// Usage: Only trigger when width changes by more than 50px
const conditionalObserver = new ConditionalResizeObserver(
(entries) => {
console.log('Conditional resize triggered');
},
(newSize, lastSize) => {
if (!lastSize) return true;
return Math.abs(newSize.width - lastSize.width) > 50;
}
);
Best Practices Summary
ResizeObserver Best Practices:
┌─────────────────────────────────────────────────────┐
│ │
│ Performance │
│ ├── Use throttle/debounce in callbacks │
│ ├── Avoid triggering layout in callbacks │
│ ├── Disconnect unneeded observations │
│ └── Batch process multiple element changes │
│ │
│ Use Cases │
│ ├── Responsive components │
│ ├── Adaptive charts │
│ ├── Virtual scrolling │
│ └── Container query alternative │
│ │
│ Considerations │
│ ├── Callbacks fire before RAF │
│ ├── Avoid circular triggers │
│ ├── Handle initial callback │
│ └── Consider devicePixelRatio │
│ │
└─────────────────────────────────────────────────────┘
| Box Model | Description | Use Case |
|---|---|---|
| content-box | Content area | Default, common |
| border-box | With border | Precise layout |
| device-pixel-content-box | Device pixels | Canvas |
Master the ResizeObserver API to build truly responsive components and layouts.