ResizeObserver in Chrome Extensions: Responsive UI Components
Building responsive user interfaces in Chrome extensions presents unique challenges that differ significantly from traditional web development. Unlike conventional websites where you control the entire viewport, Chrome extensions operate within constrained spaces—popup windows, side panels, options pages, and injected content scripts—each with its own sizing constraints. This is where ResizeObserver becomes an indispensable tool for extension developers seeking to create fluid, adaptive user interfaces.
ResizeObserver is a browser API that allows you to observe changes to an element’s dimensions, enabling your extension to respond in real-time when the containing element resizes. Whether you are building a sophisticated popup that adapts its layout based on available space, a side panel that reflows content dynamically, or an injected widget that needs to adjust its presentation based on container constraints, understanding how to implement ResizeObserver effectively will dramatically improve your extension’s user experience.
Understanding ResizeObserver Fundamentals
Before diving into extension-specific implementations, it is essential to grasp the core concepts of the ResizeObserver API. ResizeObserver provides a mechanism for observing changes to element dimensions without relying on polling or window resize events. This makes it far more efficient than traditional approaches that often resulted in performance issues and inaccurate measurements.
The API works by registering a callback function that fires whenever the observed element’s dimensions change. The callback receives an array of ResizeObserverEntry objects, each containing detailed information about the element’s new dimensions. This includes the content box size, border box size, and device pixel ratio, giving you granular control over how your UI responds to size changes.
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
console.log('Element resized:', {
contentBox: entry.contentBoxSize,
borderBox: entry.borderBoxSize,
devicePixelRatio: entry.devicePixelRatio
});
}
});
observer.observe(document.querySelector('.my-element'));
What makes ResizeObserver particularly powerful for extension development is its ability to observe any element, not just the viewport. In a Chrome extension context, this means you can observe the popup container itself, individual components within your popup, or even elements in the host page when using content scripts.
Why ResizeObserver Matters for Chrome Extensions
Chrome extensions face unique sizing challenges that make traditional responsive design approaches insufficient. When a user opens your extension’s popup, it has a fixed or constrained size. Side panels have their own sizing rules that may change based on user preferences or browser settings. Options pages might be opened in regular tabs with variable dimensions. Each of these contexts requires a different approach to responsive design.
The traditional solution was to use percentage-based widths and CSS media queries. While these work well for many scenarios, they cannot capture the actual available space within your extension’s container. A popup might have plenty of vertical space but limited horizontal room, or vice versa. CSS alone cannot easily adapt your layout based on these specific constraints.
This is where ResizeObserver shines. By observing your container element, you can programmatically adjust your UI based on the actual available space. This enables truly dynamic layouts that go beyond what CSS can achieve. You can show or hide elements based on available height, change font sizes based on available width, switch between single-column and multi-column layouts, or adjust the complexity of your interface based on the space available.
Another critical consideration is performance. Polling element dimensions using setInterval or requestAnimationFrame is computationally expensive and can negatively impact your extension’s responsiveness. ResizeObserver is designed to be highly efficient, only firing callbacks when dimensions actually change and batching multiple changes into a single callback invocation.
Implementing ResizeObserver in Extension Popups
Let’s explore how to implement ResizeObserver in a Chrome extension popup. The most common use case is creating a popup that adapts its layout based on available space, providing the best possible user experience regardless of how much space the user has allocated to your extension.
First, ensure your manifest file declares the appropriate permissions and popup configuration:
{
"manifest_version": 3,
"name": "Responsive Extension Example",
"version": "1.0",
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
}
Your popup HTML should contain a container element that will be observed:
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div id="extension-root" class="extension-container">
<header class="extension-header">
<h1>My Extension</h1>
</header>
<main id="content-area" class="extension-content">
<div class="primary-content">Main Content</div>
<div class="secondary-content">Additional Info</div>
</main>
</div>
<script src="popup.js"></script>
</body>
</html>
Now for the JavaScript implementation in popup.js:
class ResponsivePopup {
constructor() {
this.rootElement = document.getElementById('extension-root');
this.contentArea = document.getElementById('content-area');
this.observer = null;
this.init();
}
init() {
// Initialize ResizeObserver
this.observer = new ResizeObserver((entries) => {
this.handleResize(entries);
});
// Observe the root element
this.observer.observe(this.rootElement);
// Handle cleanup when popup closes
window.addEventListener('unload', () => {
this.observer.disconnect();
});
}
handleResize(entries) {
for (const entry of entries) {
const { width, height } = entry.contentBox[0];
// Adjust layout based on available space
this.adjustLayout(width, height);
}
}
adjustLayout(width, height) {
// Remove existing layout classes
this.rootElement.classList.remove('compact', 'comfortable', 'spacious');
// Apply appropriate layout class based on dimensions
if (width < 300) {
this.rootElement.classList.add('compact');
this.contentArea.style.flexDirection = 'column';
} else if (width < 450) {
this.rootElement.classList.add('comfortable');
this.contentArea.style.flexDirection = 'column';
} else {
this.rootElement.classList.add('spacious');
this.contentArea.style.flexDirection = 'row';
}
// Adjust content based on height
if (height < 400) {
this.rootElement.classList.add('short');
} else {
this.rootElement.classList.remove('short');
}
// Update CSS custom properties for dynamic sizing
document.documentElement.style.setProperty('--available-width', `${width}px`);
document.documentElement.style.setProperty('--available-height', `${height}px`);
}
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new ResponsivePopup();
});
The corresponding CSS uses these layout classes to create adaptive styles:
.extension-container {
width: 100%;
min-height: 200px;
max-height: 600px;
padding: 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
transition: all 0.3s ease;
}
.extension-container.compact {
padding: 8px;
}
.extension-container.compact .extension-header h1 {
font-size: 14px;
}
.extension-container.spacious .extension-header h1 {
font-size: 24px;
}
.extension-content {
display: flex;
flex: 1;
gap: 16px;
}
.extension-container.compact .extension-content {
gap: 8px;
}
Advanced Techniques for Dynamic Sizing
Beyond basic layout changes, ResizeObserver enables sophisticated dynamic sizing patterns that can significantly enhance your extension’s functionality. One powerful technique is implementing virtualized lists that only render visible items based on available container height.
For extensions that display lists of items—bookmarks, history entries, tabs, or any collection—virtualization is essential for performance. ResizeObserver allows you to dynamically recalculate how many items to render as the container size changes:
class VirtualizedList {
constructor(container, items, itemRenderer) {
this.container = container;
this.items = items;
this.itemRenderer = itemRenderer;
this.itemHeight = 48; // Default item height
this.visibleCount = 0;
this.scrollTop = 0;
this.observer = new ResizeObserver((entries) => {
this.calculateVisibleItems(entries);
});
this.observer.observe(this.container);
this.container.addEventListener('scroll', () => this.handleScroll());
}
calculateVisibleItems(entries) {
const { height } = entry.contentBox[0];
this.visibleCount = Math.ceil(height / this.itemHeight);
this.render();
}
render() {
const startIndex = Math.floor(this.scrollTop / this.itemHeight);
const endIndex = Math.min(
startIndex + this.visibleCount + 2,
this.items.length
);
// Clear and render only visible items
this.container.innerHTML = '';
for (let i = startIndex; i < endIndex; i++) {
const itemElement = this.itemRenderer(this.items[i]);
itemElement.style.position = 'absolute';
itemElement.style.top = `${i * this.itemHeight}px`;
itemElement.style.width = '100%';
this.container.appendChild(itemElement);
}
// Set container height to accommodate all items
this.container.style.height = `${this.items.length * this.itemHeight}px`;
}
}
Another advanced technique involves creating responsive charts and data visualizations that adapt to available space. Extensions that display analytics, statistics, or any data-driven content can use ResizeObserver to recalculate chart dimensions in real-time:
class ResponsiveChart {
constructor(chartContainer, data) {
this.container = chartContainer;
this.data = data;
this.canvas = document.createElement('canvas');
this.container.appendChild(this.canvas);
this.ctx = this.canvas.getContext('2d');
this.observer = new ResizeObserver(() => {
this.resizeCanvas();
this.render();
});
this.observer.observe(this.container);
}
resizeCanvas() {
const rect = this.container.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
this.canvas.width = rect.width * dpr;
this.canvas.height = rect.height * dpr;
this.canvas.style.width = `${rect.width}px`;
this.canvas.style.height = `${rect.height}px`;
this.ctx.scale(dpr, dpr);
}
render() {
const rect = this.container.getBoundingClientRect();
const width = rect.width;
const height = rect.height;
// Clear canvas
this.ctx.clearRect(0, 0, width, height);
// Adjust chart complexity based on available space
if (width < 200 || height < 150) {
this.renderSimpleChart(width, height);
} else if (width < 400 || height < 300) {
this.renderMediumChart(width, height);
} else {
this.renderFullChart(width, height);
}
}
}
Using ResizeObserver in Content Scripts
Content scripts present another valuable opportunity for ResizeObserver implementation. When your extension injects UI components into web pages, you often need those components to adapt to the surrounding page layout. ResizeObserver enables this by letting you observe the container element your extension creates.
Consider an extension that injects a sidebar widget into websites. The available space for your widget might change as the user resizes their browser window or as the website’s own layout adjusts. Using ResizeObserver, your widget can adapt seamlessly:
// content-script.js
class InjectedSidebar {
constructor() {
this.sidebar = document.createElement('div');
this.sidebar.className = 'my-extension-sidebar';
this.setupSidebar();
this.observeResize();
}
setupSidebar() {
this.sidebar.innerHTML = '<div class="sidebar-content">Widget Content</div>';
document.body.appendChild(this.sidebar);
// Inject styles
const style = document.createElement('style');
style.textContent = `
.my-extension-sidebar {
position: fixed;
right: 0;
top: 0;
height: 100vh;
width: 300px;
background: white;
box-shadow: -2px 0 10px rgba(0,0,0,0.1);
z-index: 999999;
transition: width 0.3s ease;
}
`;
document.head.appendChild(style);
}
observeResize() {
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const { inlineSize } = entry.borderBox[0];
// Adjust content based on available width
this.sidebar.style.width = `${Math.min(inlineSize, 400)}px`;
// Update content layout
this.updateContentLayout(inlineSize);
}
});
observer.observe(this.sidebar);
}
updateContentLayout(width) {
const content = this.sidebar.querySelector('.sidebar-content');
if (width < 250) {
content.classList.add('compact-view');
content.classList.remove('expanded-view');
} else {
content.classList.add('expanded-view');
content.classList.remove('compact-view');
}
}
}
// Initialize when DOM is ready
if (document.body) {
new InjectedSidebar();
} else {
document.addEventListener('DOMContentLoaded', () => {
new InjectedSidebar();
});
}
Best Practices and Performance Considerations
While ResizeObserver is powerful and efficient, following best practices ensures your extension remains performant and does not negatively impact the user’s browsing experience. One critical practice is to always disconnect the observer when it is no longer needed. In Chrome extensions, this is particularly important because the popup or side panel might be opened and closed frequently.
Always clean up observers in your unload event handlers:
window.addEventListener('unload', () => {
if (this.observer) {
this.observer.disconnect();
}
});
Another important consideration is debouncing your resize callbacks. While ResizeObserver is efficient, rapid size changes can still trigger many callbacks in quick succession. For complex layout adjustments, debouncing prevents UI flickering and reduces computational load:
class DebouncedResizeObserver {
constructor(callback, delay = 100) {
this.callback = callback;
this.delay = delay;
this.timeout = null;
this.observer = new ResizeObserver((entries) => {
this.debounce(entries);
});
}
debounce(entries) {
clearTimeout(this.timeout);
this.timeout = setTimeout(() => {
this.callback(entries);
}, this.delay);
}
observe(element) {
this.observer.observe(element);
}
disconnect() {
this.observer.disconnect();
clearTimeout(this.timeout);
}
}
When implementing ResizeObserver, also be mindful of the order of operations in your callback. If your callback triggers additional layout changes, you might cause layout thrashing. Use requestAnimationFrame to ensure your changes are applied at the optimal time in the browser’s rendering cycle:
handleResize(entries) {
requestAnimationFrame(() => {
for (const entry of entries {
this.adjustLayout(entry.contentBox[0]);
}
});
}
Common Pitfalls and How to Avoid Them
Several common mistakes can undermine the effectiveness of ResizeObserver in Chrome extensions. Understanding these pitfalls helps you avoid them in your own implementations.
The first pitfall is observing the wrong element. In extension popups, developers often mistakenly observe the document body or window instead of the actual container element. Remember that popup windows have their own document context, and observing the body may not give you the dimensions of the visible area. Always observe the specific container element that contains your UI components.
Another common issue is failing to handle initial size. ResizeObserver callbacks fire when size changes, but they do not automatically fire for the initial size. After setting up your observer, manually check and apply the initial layout:
this.observer = new ResizeObserver((entries) => {
this.handleResize(entries);
});
this.observer.observe(this.container);
// Handle initial size
const initialRect = this.container.getBoundingClientRect();
this.handleResize([{
contentBox: [{ inlineSize: initialRect.width, blockSize: initialRect.height }]
}]);
A third pitfall is memory leaks from failing to disconnect observers. In single-page application contexts within extensions, observers can persist longer than intended if not properly cleaned up. Always implement cleanup in your component’s destroy or unload methods.
Conclusion
ResizeObserver is an essential tool for building responsive, dynamic Chrome extensions. By understanding how to observe and respond to container dimension changes, you can create extensions that provide excellent user experiences across all contexts—popups, side panels, options pages, and injected content scripts.
The key takeaways are straightforward: observe the right container elements, respond with meaningful layout changes, clean up observers when they are no longer needed, and follow performance best practices to ensure your extension remains fast and responsive. With these techniques in your toolkit, you are well-equipped to build Chrome extensions with truly responsive user interfaces that adapt seamlessly to any container size.
As Chrome extensions continue to evolve and users expect increasingly sophisticated experiences, mastering APIs like ResizeObserver will differentiate your extensions from the competition. Start implementing these patterns in your projects today, and your users will benefit from interfaces that feel polished, professional, and perfectly adapted to their needs.