
Responsive Component Design with ResizeObserver in JavaScript
CSS @media queries have long been the foundation of responsive web design, but they share a fundamental limitation: they only measure the global browser viewport. In component-driven architectures, the exact same .product-card or .analytics-widget component might be rendered inside a narrow 280px sidebar, a 480px dashboard column, or a full-width 960px main feed on the very same screen.
Historically, developers worked around this by listening to window.addEventListener('resize', ...) and polling element.getBoundingClientRect()—a pattern that misses non-window layout changes (such as collapsing a sidebar or dynamically injecting content) and risks triggering expensive synchronous layout thrashing. With ResizeObserver now supported across Chrome, Edge, Firefox (since Firefox 69), and Safari (since Safari 13.1), components can observe their own box dimensions directly and adapt their layout without coupling to viewport breakpoints.
Example
// Observe a component container and toggle layout modifier classes
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const width = entry.contentRect.width;
entry.target.classList.toggle('card--compact', width < 360);
entry.target.classList.toggle('card--wide', width >= 600);
}
});
observer.observe(document.querySelector('.card'));
Input : A reusable UI component placed inside fluid grid tracks, collapsible sidebars, or split panes
Output : Container-aware layout states updated before the browser paints, avoiding window.onresize polling.
Implementation
To scale across multiple component instances on a page without creating an observer per element, we can share a single ResizeObserver instance and declarative data-breakpoints attributes on each responsive container.
<div class="dashboard-grid">
<!-- Narrow sidebar placement -->
<aside class="dashboard-grid__sidebar">
<article class="media-card" data-responsive-card>
<img
class="media-card__image"
src="/img/posts/resize-observer-responsive-components.webp"
alt="Minimalist curved architectural facade"
width="667"
height="518"
loading="lazy"
/>
<div class="media-card__body">
<h3 class="media-card__title">Quarterly System Telemetry</h3>
<p class="media-card__summary">
Real-time throughput and p95 latency across edge regions.
</p>
</div>
</article>
</aside>
<!-- Wide main column placement using identical markup -->
<main class="dashboard-grid__main">
<article class="media-card" data-responsive-card>
<img
class="media-card__image"
src="/img/posts/resize-observer-responsive-components.webp"
alt="Minimalist curved architectural facade"
width="667"
height="518"
loading="lazy"
/>
<div class="media-card__body">
<h3 class="media-card__title">Quarterly System Telemetry</h3>
<p class="media-card__summary">
Real-time throughput and p95 latency across edge regions.
</p>
</div>
</article>
</main>
</div>
.media-card {
display: grid;
gap: 1rem;
padding: 1rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
background-color: #ffffff;
}
/* Stacked layout when the card itself is narrow (< 420px) */
.media-card[data-layout="stacked"] {
grid-template-columns: 1fr;
}
/* Horizontal split layout when the card has >= 420px of available width */
.media-card[data-layout="horizontal"] {
grid-template-columns: 180px 1fr;
align-items: center;
}
/* Expanded feature layout when the card has >= 640px of available width */
.media-card[data-layout="feature"] {
grid-template-columns: 260px 1fr;
gap: 1.5rem;
padding: 1.5rem;
}
.media-card__image {
display: block;
width: 100%;
height: auto;
border-radius: 6px;
object-fit: cover;
}
function initResponsiveCards(selector = '[data-responsive-card]') {
if (!('ResizeObserver' in window)) {
return null;
}
const resolveLayoutState = (width) => {
if (width >= 640) return 'feature';
if (width >= 420) return 'horizontal';
return 'stacked';
};
const cardObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// Use contentBoxSize where available (ResizeObserver Level 1),
// falling back to contentRect.width for Safari 13.1 compatibility
const inlineSize = entry.contentBoxSize
? (Array.isArray(entry.contentBoxSize)
? entry.contentBoxSize[0].inlineSize
: entry.contentBoxSize.inlineSize)
: entry.contentRect.width;
const nextState = resolveLayoutState(inlineSize);
if (entry.target.dataset.layout !== nextState) {
entry.target.dataset.layout = nextState;
}
}
});
const cards = document.querySelectorAll(selector);
cards.forEach((card) => cardObserver.observe(card));
return cardObserver;
}
initResponsiveCards();
Main Point: Use a shared ResizeObserver instance to map a component's actual container width (entry.contentRect.width or contentBoxSize) to declarative data-* attributes so UI components adapt to their parent container rather than the global viewport.
Credits
Photo by Joel Filipe