JavaScript Resize Observer API: Responsive Element Monitoring
Master the Resize Observer API to efficiently detect element size changes. Learn responsive design patterns, performance optimization, and practical implementations.
The Resize Observer API provides a performant way to monitor changes to an element's size. Unlike window resize events, it can observe individual elements and fires only when an element's dimensions actually change.
Understanding Resize Observer
Resize Observer allows you to watch for changes in an element's content rectangle or border box, making it perfect for responsive components and dynamic layouts.
Basic Resize Observer Setup
// Create a ResizeObserver instance
const resizeObserver = new ResizeObserver((entries, observer) => {
entries.forEach((entry) => {
console.log('Element:', entry.target);
console.log('Content Rectangle:', entry.contentRect);
console.log('Width:', entry.contentRect.width);
console.log('Height:', entry.contentRect.height);
// New properties (if supported)
if (entry.borderBoxSize) {
console.log('Border Box Size:', entry.borderBoxSize);
}
if (entry.contentBoxSize) {
console.log('Content Box Size:', entry.contentBoxSize);
}
if (entry.devicePixelContentBoxSize) {
console.log(
'Device Pixel Content Box Size:',
entry.devicePixelContentBoxSize
);
}
});
});
// Observe an element
const targetElement = document.querySelector('.resize-me');
resizeObserver.observe(targetElement);
// Stop observing
resizeObserver.unobserve(targetElement);
// Disconnect observer completely
resizeObserver.disconnect();
// Detailed entry information
const detailedObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
// Legacy contentRect (always available)
const rect = entry.contentRect;
console.log({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
left: rect.left,
});
// Modern size arrays (check for support)
if (entry.borderBoxSize?.length) {
const borderBox = entry.borderBoxSize[0];
console.log('Border box:', {
inlineSize: borderBox.inlineSize,
blockSize: borderBox.blockSize,
});
}
if (entry.contentBoxSize?.length) {
const contentBox = entry.contentBoxSize[0];
console.log('Content box:', {
inlineSize: contentBox.inlineSize,
blockSize: contentBox.blockSize,
});
}
});
});
Advanced Configuration
// Resize Observer with options (future support)
const observer = new ResizeObserver(
(entries) => {
entries.forEach((entry) => {
handleResize(entry);
});
},
{
// Future option: box model to observe
box: 'content-box', // or 'border-box', 'device-pixel-content-box'
}
);
// Handling different writing modes
function getElementSize(entry) {
// Use modern API if available
if (entry.contentBoxSize?.length) {
const contentBox = entry.contentBoxSize[0];
return {
width: contentBox.inlineSize,
height: contentBox.blockSize,
};
}
// Fallback to contentRect
return {
width: entry.contentRect.width,
height: entry.contentRect.height,
};
}
// Device pixel ratio handling
function getDevicePixelSize(entry) {
if (entry.devicePixelContentBoxSize?.length) {
const deviceBox = entry.devicePixelContentBoxSize[0];
return {
width: deviceBox.inlineSize,
height: deviceBox.blockSize,
};
}
// Fallback calculation
const dpr = window.devicePixelRatio || 1;
return {
width: entry.contentRect.width * dpr,
height: entry.contentRect.height * dpr,
};
}
Practical Applications
Responsive Components
// Responsive card component
class ResponsiveCard {
constructor(element) {
this.element = element;
this.breakpoints = {
small: 300,
medium: 500,
large: 700,
};
this.resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
this.updateLayout(entry.contentRect.width);
});
this.resizeObserver.observe(this.element);
}
updateLayout(width) {
// Remove all size classes
Object.keys(this.breakpoints).forEach((size) => {
this.element.classList.remove(`card--${size}`);
});
// Add appropriate size class
if (width < this.breakpoints.small) {
this.element.classList.add('card--small');
} else if (width < this.breakpoints.medium) {
this.element.classList.add('card--medium');
} else {
this.element.classList.add('card--large');
}
// Update internal layout
this.adjustContent(width);
}
adjustContent(width) {
const title = this.element.querySelector('.card__title');
const description = this.element.querySelector('.card__description');
if (width < this.breakpoints.small) {
// Truncate text for small sizes
this.truncateText(title, 30);
this.truncateText(description, 50);
} else {
// Restore full text
this.restoreText(title);
this.restoreText(description);
}
}
truncateText(element, maxLength) {
if (!element) return;
const fullText =
element.getAttribute('data-full-text') || element.textContent;
element.setAttribute('data-full-text', fullText);
if (fullText.length > maxLength) {
element.textContent = fullText.substring(0, maxLength) + '...';
}
}
restoreText(element) {
if (!element) return;
const fullText = element.getAttribute('data-full-text');
if (fullText) {
element.textContent = fullText;
}
}
destroy() {
this.resizeObserver.disconnect();
}
}
// Container query polyfill using ResizeObserver
class ContainerQuery {
constructor(element, queries) {
this.element = element;
this.queries = queries;
this.currentQuery = null;
this.resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
this.evaluateQueries(entry.contentRect);
});
this.resizeObserver.observe(this.element);
// Initial evaluation
const rect = this.element.getBoundingClientRect();
this.evaluateQueries(rect);
}
evaluateQueries(rect) {
const width = rect.width;
const height = rect.height;
// Find matching query
let matchedQuery = null;
for (const query of this.queries) {
if (this.matchesQuery(query, width, height)) {
matchedQuery = query;
}
}
// Apply styles if query changed
if (matchedQuery !== this.currentQuery) {
this.applyQuery(matchedQuery);
this.currentQuery = matchedQuery;
}
}
matchesQuery(query, width, height) {
let matches = true;
if (query.minWidth !== undefined && width < query.minWidth) {
matches = false;
}
if (query.maxWidth !== undefined && width > query.maxWidth) {
matches = false;
}
if (query.minHeight !== undefined && height < query.minHeight) {
matches = false;
}
if (query.maxHeight !== undefined && height > query.maxHeight) {
matches = false;
}
return matches;
}
applyQuery(query) {
// Remove all query classes
this.queries.forEach((q) => {
if (q.className) {
this.element.classList.remove(q.className);
}
});
// Apply new query
if (query) {
if (query.className) {
this.element.classList.add(query.className);
}
if (query.styles) {
Object.assign(this.element.style, query.styles);
}
if (query.callback) {
query.callback(this.element);
}
}
}
destroy() {
this.resizeObserver.disconnect();
}
}
// Usage
const containerQuery = new ContainerQuery(element, [
{
minWidth: 0,
maxWidth: 399,
className: 'container--small',
styles: { fontSize: '14px' },
},
{
minWidth: 400,
maxWidth: 799,
className: 'container--medium',
styles: { fontSize: '16px' },
},
{
minWidth: 800,
className: 'container--large',
styles: { fontSize: '18px' },
callback: (el) => console.log('Large container activated'),
},
]);
Dynamic Layouts
// Auto-sizing textarea
class AutoSizeTextarea {
constructor(textarea) {
this.textarea = textarea;
this.minHeight = parseInt(getComputedStyle(textarea).minHeight) || 50;
this.maxHeight = parseInt(getComputedStyle(textarea).maxHeight) || 300;
// Create hidden clone for measuring
this.clone = this.createClone();
// Set up resize observer on clone
this.resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
this.updateHeight(entry.contentRect.height);
});
// Observe the clone
this.resizeObserver.observe(this.clone);
// Listen for input
this.textarea.addEventListener('input', () => this.adjustSize());
// Initial adjustment
this.adjustSize();
}
createClone() {
const clone = document.createElement('div');
clone.className = 'textarea-clone';
clone.style.cssText = `
position: absolute;
top: -9999px;
left: -9999px;
visibility: hidden;
white-space: pre-wrap;
word-wrap: break-word;
`;
// Copy styles
const styles = getComputedStyle(this.textarea);
[
'fontFamily',
'fontSize',
'fontWeight',
'lineHeight',
'padding',
'border',
'width',
].forEach((prop) => {
clone.style[prop] = styles[prop];
});
document.body.appendChild(clone);
return clone;
}
adjustSize() {
// Update clone content
this.clone.textContent = this.textarea.value || 'x'; // 'x' ensures minimum height
// The resize observer will handle the height update
}
updateHeight(height) {
const newHeight = Math.min(
Math.max(height, this.minHeight),
this.maxHeight
);
this.textarea.style.height = `${newHeight}px`;
}
destroy() {
this.resizeObserver.disconnect();
this.clone.remove();
}
}
// Grid layout manager
class GridLayoutManager {
constructor(container, options = {}) {
this.container = container;
this.options = {
minItemWidth: 250,
gap: 16,
...options,
};
this.resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
this.updateLayout(entry.contentRect.width);
});
this.resizeObserver.observe(this.container);
}
updateLayout(containerWidth) {
const availableWidth = containerWidth - this.options.gap;
const minWidth = this.options.minItemWidth;
// Calculate optimal number of columns
let columns = Math.floor(availableWidth / (minWidth + this.options.gap));
columns = Math.max(1, columns);
// Calculate actual item width
const itemWidth =
(availableWidth - (columns - 1) * this.options.gap) / columns;
// Update CSS custom properties
this.container.style.setProperty('--grid-columns', columns);
this.container.style.setProperty('--item-width', `${itemWidth}px`);
this.container.style.setProperty('--gap', `${this.options.gap}px`);
// Apply grid styles
this.container.style.display = 'grid';
this.container.style.gridTemplateColumns = `repeat(${columns}, 1fr)`;
this.container.style.gap = `${this.options.gap}px`;
// Notify about layout change
this.container.dispatchEvent(
new CustomEvent('layoutchange', {
detail: { columns, itemWidth },
})
);
}
destroy() {
this.resizeObserver.disconnect();
}
}
// Masonry layout with ResizeObserver
class MasonryLayout {
constructor(container, options = {}) {
this.container = container;
this.options = {
columnWidth: 300,
gap: 16,
...options,
};
this.items = [];
this.columns = [];
this.resizeObserver = null;
this.itemObserver = null;
this.init();
}
init() {
// Observe container
this.resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
this.onContainerResize(entry.contentRect.width);
});
this.resizeObserver.observe(this.container);
// Observe individual items
this.itemObserver = new ResizeObserver((entries) => {
entries.forEach((entry) => {
this.onItemResize(entry.target);
});
});
// Initial setup
this.setupItems();
}
setupItems() {
this.items = Array.from(this.container.children);
this.items.forEach((item) => {
item.style.position = 'absolute';
item.style.width = `${this.options.columnWidth}px`;
this.itemObserver.observe(item);
});
}
onContainerResize(width) {
const columnCount = Math.floor(
width / (this.options.columnWidth + this.options.gap)
);
if (columnCount !== this.columns.length) {
this.setupColumns(columnCount);
this.layoutItems();
}
}
onItemResize(item) {
// Relayout when item size changes
this.layoutItems();
}
setupColumns(count) {
this.columns = Array(count).fill(0);
}
layoutItems() {
this.columns.fill(0);
this.items.forEach((item) => {
// Find shortest column
const shortestColumn = this.columns.indexOf(Math.min(...this.columns));
// Position item
const x = shortestColumn * (this.options.columnWidth + this.options.gap);
const y = this.columns[shortestColumn];
item.style.transform = `translate(${x}px, ${y}px)`;
// Update column height
const itemHeight = item.getBoundingClientRect().height;
this.columns[shortestColumn] += itemHeight + this.options.gap;
});
// Update container height
const maxHeight = Math.max(...this.columns);
this.container.style.height = `${maxHeight}px`;
}
addItem(element) {
this.items.push(element);
element.style.position = 'absolute';
element.style.width = `${this.options.columnWidth}px`;
this.itemObserver.observe(element);
this.layoutItems();
}
removeItem(element) {
const index = this.items.indexOf(element);
if (index > -1) {
this.items.splice(index, 1);
this.itemObserver.unobserve(element);
this.layoutItems();
}
}
destroy() {
this.resizeObserver.disconnect();
this.itemObserver.disconnect();
}
}
Chart and Visualization Resizing
// Responsive chart component
class ResponsiveChart {
constructor(container, data, options = {}) {
this.container = container;
this.data = data;
this.options = options;
this.chart = null;
this.resizeTimeout = null;
this.canvas = document.createElement('canvas');
this.container.appendChild(this.canvas);
this.resizeObserver = new ResizeObserver((entries) => {
this.handleResize(entries[0]);
});
this.resizeObserver.observe(this.container);
// Initial render
this.render();
}
handleResize(entry) {
// Debounce resize events
clearTimeout(this.resizeTimeout);
this.resizeTimeout = setTimeout(() => {
const { width, height } = entry.contentRect;
this.updateDimensions(width, height);
this.render();
}, 100);
}
updateDimensions(width, height) {
// Update canvas size
this.canvas.width = width * window.devicePixelRatio;
this.canvas.height = height * window.devicePixelRatio;
this.canvas.style.width = `${width}px`;
this.canvas.style.height = `${height}px`;
// Scale for device pixel ratio
const ctx = this.canvas.getContext('2d');
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
// Update chart dimensions
if (this.chart) {
this.chart.resize(width, height);
}
}
render() {
const ctx = this.canvas.getContext('2d');
const width = this.canvas.width / window.devicePixelRatio;
const height = this.canvas.height / window.devicePixelRatio;
// Clear canvas
ctx.clearRect(0, 0, width, height);
// Simple bar chart example
const barWidth = width / this.data.length;
const maxValue = Math.max(...this.data.map((d) => d.value));
this.data.forEach((item, index) => {
const barHeight = (item.value / maxValue) * height * 0.8;
const x = index * barWidth + barWidth * 0.1;
const y = height - barHeight;
ctx.fillStyle = item.color || '#007bff';
ctx.fillRect(x, y, barWidth * 0.8, barHeight);
// Add label if space permits
if (barWidth > 50) {
ctx.fillStyle = '#333';
ctx.font = '12px Arial';
ctx.textAlign = 'center';
ctx.fillText(item.label, x + barWidth * 0.4, height - 5);
}
});
}
updateData(newData) {
this.data = newData;
this.render();
}
destroy() {
clearTimeout(this.resizeTimeout);
this.resizeObserver.disconnect();
this.canvas.remove();
}
}
// SVG responsive graphics
class ResponsiveSVG {
constructor(container, svgContent) {
this.container = container;
this.svg = this.createSVG(svgContent);
this.container.appendChild(this.svg);
this.resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
this.updateViewBox(entry.contentRect);
});
this.resizeObserver.observe(this.container);
}
createSVG(content) {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.innerHTML = content;
svg.style.width = '100%';
svg.style.height = '100%';
return svg;
}
updateViewBox(rect) {
const aspectRatio = rect.width / rect.height;
const originalAspectRatio = 16 / 9; // Example original aspect ratio
let viewBox;
if (aspectRatio > originalAspectRatio) {
// Container is wider - fit height
const width = rect.height * originalAspectRatio;
const x = (rect.width - width) / 2;
viewBox = `${-x} 0 ${rect.width} ${rect.height}`;
} else {
// Container is taller - fit width
const height = rect.width / originalAspectRatio;
const y = (rect.height - height) / 2;
viewBox = `0 ${-y} ${rect.width} ${rect.height}`;
}
this.svg.setAttribute('viewBox', viewBox);
}
destroy() {
this.resizeObserver.disconnect();
}
}
Performance Monitoring
// Element size performance monitor
class SizePerformanceMonitor {
constructor() {
this.metrics = new Map();
this.observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
this.recordMetric(entry);
});
});
}
monitor(element, name) {
if (!this.metrics.has(element)) {
this.metrics.set(element, {
name,
resizeCount: 0,
sizes: [],
lastResize: null,
totalResizeTime: 0,
});
}
this.observer.observe(element);
}
recordMetric(entry) {
const metric = this.metrics.get(entry.target);
if (!metric) return;
const now = performance.now();
const size = {
width: entry.contentRect.width,
height: entry.contentRect.height,
timestamp: now,
};
// Update metrics
metric.resizeCount++;
metric.sizes.push(size);
if (metric.lastResize) {
metric.totalResizeTime += now - metric.lastResize;
}
metric.lastResize = now;
// Keep only last 100 sizes
if (metric.sizes.length > 100) {
metric.sizes.shift();
}
// Check for rapid resizing
if (metric.resizeCount > 10 && metric.totalResizeTime < 1000) {
console.warn(`Rapid resizing detected for ${metric.name}`);
}
}
getReport(element) {
const metric = this.metrics.get(element);
if (!metric) return null;
const sizes = metric.sizes;
const widths = sizes.map((s) => s.width);
const heights = sizes.map((s) => s.height);
return {
name: metric.name,
resizeCount: metric.resizeCount,
averageWidth: widths.reduce((a, b) => a + b, 0) / widths.length,
averageHeight: heights.reduce((a, b) => a + b, 0) / heights.length,
minWidth: Math.min(...widths),
maxWidth: Math.max(...widths),
minHeight: Math.min(...heights),
maxHeight: Math.max(...heights),
resizeFrequency: metric.resizeCount / (metric.totalResizeTime / 1000), // per second
};
}
getAllReports() {
const reports = [];
this.metrics.forEach((metric, element) => {
reports.push(this.getReport(element));
});
return reports;
}
stopMonitoring(element) {
this.observer.unobserve(element);
this.metrics.delete(element);
}
destroy() {
this.observer.disconnect();
this.metrics.clear();
}
}
// Layout thrashing detector
class LayoutThrashingDetector {
constructor(threshold = 10) {
this.threshold = threshold;
this.resizeEvents = new Map();
this.thrashingElements = new Set();
this.observer = new ResizeObserver((entries) => {
const now = performance.now();
entries.forEach((entry) => {
const element = entry.target;
if (!this.resizeEvents.has(element)) {
this.resizeEvents.set(element, []);
}
const events = this.resizeEvents.get(element);
events.push(now);
// Keep only events from last second
const oneSecondAgo = now - 1000;
const recentEvents = events.filter((time) => time > oneSecondAgo);
this.resizeEvents.set(element, recentEvents);
// Check for thrashing
if (recentEvents.length > this.threshold) {
if (!this.thrashingElements.has(element)) {
this.thrashingElements.add(element);
this.onThrashingDetected(element, recentEvents.length);
}
} else if (this.thrashingElements.has(element)) {
this.thrashingElements.delete(element);
this.onThrashingResolved(element);
}
});
});
}
observe(element) {
this.observer.observe(element);
}
onThrashingDetected(element, count) {
console.warn(`Layout thrashing detected on element:`, element);
console.warn(`${count} resize events in the last second`);
element.dispatchEvent(
new CustomEvent('layoutthrashing', {
detail: { count },
bubbles: true,
})
);
}
onThrashingResolved(element) {
console.log('Layout thrashing resolved for element:', element);
element.dispatchEvent(
new CustomEvent('thrashingresolved', {
bubbles: true,
})
);
}
getStats() {
const stats = [];
this.resizeEvents.forEach((events, element) => {
stats.push({
element,
recentResizes: events.length,
isThrashing: this.thrashingElements.has(element),
});
});
return stats;
}
destroy() {
this.observer.disconnect();
this.resizeEvents.clear();
this.thrashingElements.clear();
}
}
Advanced Patterns
Responsive Table
// Responsive table that adapts based on available space
class ResponsiveTable {
constructor(table) {
this.table = table;
this.columns = this.analyzeColumns();
this.priorityColumns = this.setPriorities();
this.resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
this.adaptTable(entry.contentRect.width);
});
this.resizeObserver.observe(this.table);
}
analyzeColumns() {
const headers = Array.from(this.table.querySelectorAll('thead th'));
return headers.map((header, index) => ({
index,
header,
priority: parseInt(header.dataset.priority) || 5,
minWidth: parseInt(header.dataset.minWidth) || 100,
text: header.textContent,
visible: true,
}));
}
setPriorities() {
return [...this.columns].sort((a, b) => b.priority - a.priority);
}
adaptTable(availableWidth) {
let currentWidth = 0;
const padding = 20; // Account for cell padding
// Reset all columns to visible
this.columns.forEach((col) => {
col.visible = true;
this.showColumn(col.index);
});
// Calculate which columns fit
for (const column of this.priorityColumns) {
const columnWidth = column.minWidth + padding;
if (currentWidth + columnWidth <= availableWidth) {
currentWidth += columnWidth;
} else {
column.visible = false;
this.hideColumn(column.index);
}
}
// Update mobile view if needed
if (currentWidth > availableWidth) {
this.enableMobileView();
} else {
this.disableMobileView();
}
}
hideColumn(index) {
// Hide header
const headers = this.table.querySelectorAll('thead th');
if (headers[index]) {
headers[index].style.display = 'none';
}
// Hide cells
const rows = this.table.querySelectorAll('tbody tr');
rows.forEach((row) => {
const cells = row.querySelectorAll('td');
if (cells[index]) {
cells[index].style.display = 'none';
}
});
}
showColumn(index) {
// Show header
const headers = this.table.querySelectorAll('thead th');
if (headers[index]) {
headers[index].style.display = '';
}
// Show cells
const rows = this.table.querySelectorAll('tbody tr');
rows.forEach((row) => {
const cells = row.querySelectorAll('td');
if (cells[index]) {
cells[index].style.display = '';
}
});
}
enableMobileView() {
this.table.classList.add('table--mobile');
// Transform to card layout
const rows = this.table.querySelectorAll('tbody tr');
rows.forEach((row) => {
row.classList.add('table__card');
const cells = row.querySelectorAll('td');
cells.forEach((cell, index) => {
const column = this.columns[index];
if (column && column.visible) {
cell.setAttribute('data-label', column.text);
}
});
});
}
disableMobileView() {
this.table.classList.remove('table--mobile');
const rows = this.table.querySelectorAll('tbody tr');
rows.forEach((row) => {
row.classList.remove('table__card');
const cells = row.querySelectorAll('td');
cells.forEach((cell) => {
cell.removeAttribute('data-label');
});
});
}
destroy() {
this.resizeObserver.disconnect();
this.disableMobileView();
// Show all columns
this.columns.forEach((col) => this.showColumn(col.index));
}
}
// Responsive navigation menu
class ResponsiveNav {
constructor(nav) {
this.nav = nav;
this.items = Array.from(nav.querySelectorAll('.nav__item'));
this.moreButton = this.createMoreButton();
this.moreMenu = this.createMoreMenu();
this.resizeObserver = new ResizeObserver((entries) => {
const entry = entries[0];
this.updateLayout(entry.contentRect.width);
});
this.resizeObserver.observe(this.nav);
}
createMoreButton() {
const button = document.createElement('button');
button.className = 'nav__more';
button.innerHTML = '⋯ More';
button.style.display = 'none';
button.addEventListener('click', () => this.toggleMoreMenu());
this.nav.appendChild(button);
return button;
}
createMoreMenu() {
const menu = document.createElement('div');
menu.className = 'nav__more-menu';
menu.style.display = 'none';
document.body.appendChild(menu);
return menu;
}
updateLayout(availableWidth) {
let currentWidth = 0;
const moreButtonWidth = 80;
const visibleItems = [];
const hiddenItems = [];
// Reset all items
this.items.forEach((item) => {
item.style.display = '';
const itemWidth = item.getBoundingClientRect().width;
if (currentWidth + itemWidth + moreButtonWidth <= availableWidth) {
currentWidth += itemWidth;
visibleItems.push(item);
} else {
hiddenItems.push(item);
}
});
// Update visibility
if (hiddenItems.length > 0) {
this.moreButton.style.display = '';
this.updateMoreMenu(hiddenItems);
// Hide overflow items
hiddenItems.forEach((item) => {
item.style.display = 'none';
});
} else {
this.moreButton.style.display = 'none';
this.moreMenu.style.display = 'none';
}
}
updateMoreMenu(items) {
this.moreMenu.innerHTML = '';
items.forEach((item) => {
const clone = item.cloneNode(true);
clone.style.display = '';
this.moreMenu.appendChild(clone);
});
}
toggleMoreMenu() {
const isVisible = this.moreMenu.style.display === 'block';
this.moreMenu.style.display = isVisible ? 'none' : 'block';
if (!isVisible) {
this.positionMoreMenu();
}
}
positionMoreMenu() {
const buttonRect = this.moreButton.getBoundingClientRect();
this.moreMenu.style.position = 'absolute';
this.moreMenu.style.top = `${buttonRect.bottom}px`;
this.moreMenu.style.right = `${window.innerWidth - buttonRect.right}px`;
}
destroy() {
this.resizeObserver.disconnect();
this.moreButton.remove();
this.moreMenu.remove();
}
}
Best Practices
- Debounce resize handlers for performance
class DebouncedResizeObserver {
constructor(callback, delay = 100) {
this.callback = callback;
this.delay = delay;
this.timeouts = new Map();
this.observer = new ResizeObserver((entries) => {
entries.forEach((entry) => {
if (this.timeouts.has(entry.target)) {
clearTimeout(this.timeouts.get(entry.target));
}
const timeout = setTimeout(() => {
this.callback([entry]);
this.timeouts.delete(entry.target);
}, this.delay);
this.timeouts.set(entry.target, timeout);
});
});
}
observe(target) {
this.observer.observe(target);
}
unobserve(target) {
this.observer.unobserve(target);
if (this.timeouts.has(target)) {
clearTimeout(this.timeouts.get(target));
this.timeouts.delete(target);
}
}
disconnect() {
this.observer.disconnect();
this.timeouts.forEach((timeout) => clearTimeout(timeout));
this.timeouts.clear();
}
}
- Use contentRect for dimensions
// Good - use contentRect
const width = entry.contentRect.width;
const height = entry.contentRect.height;
// Avoid - getBoundingClientRect causes reflow
const rect = entry.target.getBoundingClientRect();
- Handle device pixel ratio for canvas
function setupCanvas(canvas, width, height) {
const dpr = window.devicePixelRatio || 1;
canvas.width = width * dpr;
canvas.height = height * dpr;
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
return ctx;
}
- Clean up observers
class Component {
constructor() {
this.resizeObserver = new ResizeObserver(this.handleResize);
}
connect() {
this.resizeObserver.observe(this.element);
}
disconnect() {
this.resizeObserver.disconnect();
}
}
Conclusion
The Resize Observer API provides an efficient way to monitor element size changes, enabling truly responsive components that adapt to their container rather than just the viewport. It's essential for modern web applications that need dynamic layouts, responsive visualizations, and container-based responsive design. By following best practices and understanding its capabilities, you can create highly adaptive user interfaces that provide optimal experiences across all screen sizes and container dimensions.